webgui/lib/WebGUI/Page.pm
2004-03-02 21:18:18 +00:00

703 lines
20 KiB
Perl

package WebGUI::Page;
=head1 LEGAL
-------------------------------------------------------------------
WebGUI is Copyright 2001-2004 Plain Black LLC.
-------------------------------------------------------------------
Please read the legal notices (docs/legal.txt) and the license
(docs/license.txt) that came with this distribution before using
this software.
-------------------------------------------------------------------
http://www.plainblack.com info@plainblack.com
-------------------------------------------------------------------
=cut
use warnings;
use HTML::Template;
use strict;
use Tie::IxHash;
use WebGUI::ErrorHandler;
use WebGUI::HTMLForm;
use WebGUI::Icon;
use WebGUI::Persistent::Tree;
use WebGUI::Session;
use WebGUI::SQL;
use WebGUI::Template;
use WebGUI::Utility;
use WebGUI::DateTime;
use Data::Serializer;
our @ISA = qw(WebGUI::Persistent::Tree);
=head1 NAME
Package WebGUI::Page
=head1 DESCRIPTION
This package provides utility functions for WebGUI's page system. Some of these work in a
non-object oriented fashion. These are utility functions, not affecting the page tree hiearchy.
The methods that do affect this hiearchy should be called in a object oriented context.
=head1 SYNOPSIS
Non OO functions
use WebGUI::Page;
$integer = WebGUI::Page::countTemplatePositions($templateId);
$html = WebGUI::Page::drawTemplate($templateId);
$html = WebGUI::Page::generate();
$hashRef = WebGUI::Page::getTemplateList();
$template = WebGUI::Page::getTemplate();
$hashRef = WebGUI::Page::getTemplatePositions($templateId);
$url = WebGUI::Page::makeUnique($url,$pageId);
OO style methods
use WebGUI::Page;
$page = WebGUI::Page->getPage($pageId);
$newMother = WebGUI::Page->getPage($anotherPageId);
$page->cut;
$page->paste($newMother);
$page->set; # this automatically recaches the pagetree
$page->setWithoutRecache;
WebGUI::Page->recachePageTree # here we've got to recache manually
=head1 METHODS
These functions are available from this package:
=cut
#-------------------------------------------------------------------
sub classSettings {
return {
properties => {
# These fields define the place in the pagetree of a page
pageId => { key => 1 },
parentId => { defaultValue => 0 },
sequenceNumber => { defaultValue => 1 },
# These are the entries that define the privileges and behaviour of a page.
# They're in the same order as they apear in the set-statement in the www_editPageSave method
# of WebGUI::Operation::Page. Please keep them in this order. You will find it to be more
# convient when debugging.
title => { quote => 1 },
styleId => { defaultValue => 0 },
printableStyleId => { defaultValue => 0 },
ownerId => { defaultValue => 0 },
groupIdView => { defaultValue => 3 },
groupIdEdit => { defaultValue => 3 },
newWindow => { defaultValue => 0 },
wobjectPrivileges => { defaultValue => 0 },
hideFromNavigation => { defaultValue => 0 },
startDate => { defaultValue => 946710000 },
endDate => { defaultValue => 2082783600 },
cacheTimeout => { defaultValue => 60},
cacheTimeoutVisitor => { defaultValue => 600},
metaTags => { quote => 1 },
urlizedTitle => { quote => 1 },
redirectURL => { quote => 1 },
languageId => { defaultValue => 1 },
defaultMetaTags => { defaultValue => 0 },
templateId => { defaultValue => 1 },
menuTitle => { quote => 1 },
synopsis => { quote => 1 },
# The clipboard/trash properties.
bufferUserId => { },
bufferDate => { },
bufferPrevId => { },
# The userdefined database entries.
userDefined1 => { quote => 1 },
userDefined2 => { quote => 1 },
userDefined3 => { quote => 1 },
userDefined4 => { quote => 1 },
userDefined5 => { quote => 1 }
},
useDummyRoot => 1,
table => 'page'
}
}
#-------------------------------------------------------------------
=head2 add ( page )
Adds page to the children of the object this method is invoked on.
=over
=item page
A WebGUI::Page instance to be added to the children of the current object.
=back
=cut
sub add {
my ($self, $page, @daughters, $newSequenceNumber);
($self) = @_;
@daughters = $self->daughters;
$newSequenceNumber = 1;
$newSequenceNumber = (pop(@daughters))[0]->get('sequenceNumber') + 1 if (@daughters);
$page = WebGUI::Page->new(-properties => {
parentId => $self->get("pageId"),
sequenceNumber => $newSequenceNumber
});
$self->add_daughter($page);
$page->set;
return $page;
}
#-------------------------------------------------------------------
=head2 countTemplatePositions ( templateId )
Returns the number of template positions in the specified page template.
=over
=item templateId
The id of the page template you wish to count.
=back
=cut
sub countTemplatePositions {
my ($template, $i);
$template = getTemplate($_[0]);
$i = 1;
while ($template =~ m/position$i\_loop/) {
$i++;
}
return $i-1;
}
#-------------------------------------------------------------------
=head2 cut
Cuts the this Page object and places it on the clipboard.
=back
=cut
sub cut {
my ($self, $clipboard, $parentId);
$self = shift;
$parentId = $self->get("parentId");
# Place page in clipboard (pageId 2)
$clipboard = WebGUI::Page->getPage(2);
if ($self->move($clipboard)) {
$self->set({
bufferUserId => $session{user}{userId},
bufferDate => time,
bufferPrevId => $parentId,
});
}
return $self;
}
#-------------------------------------------------------------------
=head2 deCache ( [ pageId ] )
Deletes the cached version of a specified page. Note that this is something else than the
cached page tree. This funtion should be invoked in a non-OO context;
=over
=item pageId
The id of the page to decache. Defaults to the current page id.
=back
=cut
sub deCache {
my $cache = WebGUI::Cache->new;
my $pageId = $_[0] || $session{page}{pageId};
$cache->deleteByRegex("m/^page_".$pageId."_\\d+\$/");
}
#-------------------------------------------------------------------
=head2 delete
Deletes this Page object from the tree and places it in the trash. To physically remove
pages from the tree and the database you should use the purge method.
This function overloads the delete inherited from WebGUI::Persistent::Tree. This is done
because 'delete' is the right name for the functionality within WebGUI and because the
delete of WG::P::Tree is not very useful: it only works on one page and causes pages with
'zombie' parents.
=back
=cut
sub delete {
my ($self, $trash, $parentId);
$self = shift;
$parentId = $self->get("parentId");
# Place page in trash (pageId 3)
$trash = WebGUI::Page->getPage(3);
if ($self->move($trash)) {
$self->set({
bufferUserId => $session{user}{userId},
bufferDate => time,
bufferPrevId => $parentId,
});
}
return $self;
}
#-------------------------------------------------------------------
=head2 drawTemplate ( templateId )
Returns an HTML string containing a small representation of the page template.
=over
=item templateId
The id of the page template you wish to draw.
=back
=cut
sub drawTemplate {
my $template = getTemplate($_[0]);
$template =~ s/\n//g;
$template =~ s/\r//g;
$template =~ s/\'/\\\'/g;
$template = WebGUI::Macro::negate($template);
$template =~ s/\<script.*?\>.*?\<\/script\>//gi;
$template =~ s/\<table.*?\>/\<table cellspacing=0 cellpadding=3 width=100 height=80 border=1\>/ig;
$template =~ s/\<tmpl_loop\s+position(\d+)\_loop\>.*?\<\/tmpl\_loop\>/$1/ig;
return $template;
}
#-------------------------------------------------------------------
=head2 generate ( )
Generates the content of the page.
=cut
sub generate {
return WebGUI::Privilege::noAccess() unless (WebGUI::Privilege::canViewPage());
my %var;
$var{'page.canEdit'} = WebGUI::Privilege::canEditPage();
$var{'page.controls'} = pageIcon()
.deleteIcon('op=deletePage')
.editIcon('op=editPage')
.moveUpIcon('op=movePageUp')
.moveDownIcon('op=movePageDown')
.cutIcon('op=cutPage');
my $sth = WebGUI::SQL->read("select * from wobject where pageId=".$session{page}{pageId}." order by sequenceNumber, wobjectId");
while (my $wobject = $sth->hashRef) {
my $wobjectToolbar = wobjectIcon()
.deleteIcon('func=delete&wid='.${$wobject}{wobjectId})
.editIcon('func=edit&wid='.${$wobject}{wobjectId})
.moveUpIcon('func=moveUp&wid='.${$wobject}{wobjectId})
.moveDownIcon('func=moveDown&wid='.${$wobject}{wobjectId})
.moveTopIcon('func=moveTop&wid='.${$wobject}{wobjectId})
.moveBottomIcon('func=moveBottom&wid='.${$wobject}{wobjectId})
.cutIcon('func=cut&wid='.${$wobject}{wobjectId})
.copyIcon('func=copy&wid='.${$wobject}{wobjectId});
if (${$wobject}{namespace} ne "WobjectProxy" && isIn("WobjectProxy",@{$session{config}{wobjects}})) {
$wobjectToolbar .= shortcutIcon('func=createShortcut&wid='.${$wobject}{wobjectId});
}
if (${$wobject}{namespace} eq "WobjectProxy") {
my $originalWobject = $wobject;
my ($wobjectProxy) = WebGUI::SQL->quickHashRef("select * from WobjectProxy where wobjectId=".${$wobject}{wobjectId});
$wobject = WebGUI::SQL->quickHashRef("select * from wobject where wobject.wobjectId=".$wobjectProxy->{proxiedWobjectId});
if (${$wobject}{namespace} eq "") {
$wobject = $originalWobject;
} else {
${$wobject}{startDate} = ${$originalWobject}{startDate};
${$wobject}{endDate} = ${$originalWobject}{endDate};
${$wobject}{templatePosition} = ${$originalWobject}{templatePosition};
${$wobject}{_WobjectProxy} = ${$originalWobject}{wobjectId};
if ($wobjectProxy->{overrideTitle}) {
${$wobject}{title} = ${$originalWobject}{title};
}
if ($wobjectProxy->{overrideDisplayTitle}) {
${$wobject}{displayTitle} = ${$originalWobject}{displayTitle};
}
if ($wobjectProxy->{overrideDescription}) {
${$wobject}{description} = ${$originalWobject}{description};
}
if ($wobjectProxy->{overrideTemplate}) {
${$wobject}{templateId} = $wobjectProxy->{proxiedTemplateId};
}
}
}
my $cmd = "WebGUI::Wobject::".${$wobject}{namespace};
my $w = eval{$cmd->new($wobject)};
WebGUI::ErrorHandler::fatalError("Couldn't instanciate wobject: ${$wobject}{namespace}. Root cause: ".$@) if($@);
push(@{$var{'position'.$wobject->{templatePosition}.'_loop'}},{
'wobject.canView'=>WebGUI::Privilege::canViewWobject($wobject->{wobjectId}),
'wobject.canEdit'=>WebGUI::Privilege::canEditWobject($wobject->{wobjectId}),
'wobject.controls'=>$wobjectToolbar,
'wobject.controls.drag'=>dragIcon(),
'wobject.namespace'=>$wobject->{namespace},
'wobject.id'=>$wobject->{wobjectId},
'wobject.isInDateRange'=>$w->inDateRange,
'wobject.content'=>eval{$w->www_view}
});
WebGUI::ErrorHandler::fatalError("Wobject runtime error: ${$wobject}{namespace}. Root cause: ".$@) if($@);
}
$sth->finish;
return WebGUI::Template::process(getTemplate(),\%var);
}
#-------------------------------------------------------------------
=head2 getTemplateList
Returns a hash reference containing template ids and template titles for all the page templates available in the system.
=cut
sub getTemplateList {
return WebGUI::Template::getList("page");
}
#-------------------------------------------------------------------
=head2 getTemplate ( [ templateId ] )
Returns an HTML template.
=over
=item templateId
The id of the page template you wish to retrieve. Defaults to the current page's template id.
=back
=cut
sub getTemplate {
my $templateId = $_[0] || $session{page}{templateId};
return WebGUI::Template::get($templateId,"page");
}
#-------------------------------------------------------------------
=head2 getTemplatePositions ( templateId )
Returns a hash reference containing the positions available in the specified page template.
=over
=item templateId
The id of the page template you wish to retrieve the positions from.
=back
=cut
sub getTemplatePositions {
my (%hash, $template, $i);
tie %hash, "Tie::IxHash";
for ($i=1; $i<=countTemplatePositions($_[0]); $i++) {
$hash{$i} = $i;
}
return \%hash;
}
#-------------------------------------------------------------------
=head2 makeUnique ( pageURL, pageId )
Returns a unique page URL.
=over
=item url
The URL you're hoping for.
=item pageId
The page id of the page you're creating a URL for.
=back
=cut
sub makeUnique {
my ($url, $test, $pageId);
$url = $_[0];
$pageId = $_[1] || "new";
while (($test) = WebGUI::SQL->quickArray("select urlizedTitle from page where urlizedTitle='$url' and pageId<>'$pageId'")) {
if ($url =~ /(.*)(\d+$)/) {
$url = $1.($2+1);
} elsif ($test ne "") {
$url .= "2";
}
}
return $url;
}
#-------------------------------------------------------------------
=head2 getPage ( [pageId] )
This method fetches a WebGUI::Page object. If no pageId is given the current page will be used.
=over
=item pageId
The id of the page requested.
=back
=cut
sub getPage {
my ($cache, $pageLookup, $node, $self, $pageId, $tree, $pageInBranche);
($self, $pageId) = @_;
unless (defined $pageId) {
$pageId = $session{page}{pageId};
}
WebGUI::ErrorHandler::fatalError("Illegal pageId: '$pageId'") unless ($pageId =~ /^-?\d+$/);
# Fetch the correct pagetree from cache
$cache = WebGUI::Cache->new('root-0','PageTree-'.$session{config}{configFile});
$tree = $cache->getDataStructure;
# If the tree is cached then use it.
if (defined $tree) {
$node = $tree->{$pageId};
}
# If the tree isn't cached or if the requested page is not in the cached tree, recache.
if (!defined $tree || !$node) {
# Handle cache miss. The way it's done here costs twice the amount of time that's needed to build
# a tree. This shouldn't matter that much, though, since cache-misses should occur almost never.
WebGUI::Page->recachePageTree;
$node = WebGUI::Page->getTree()->{$pageId};
undef $pageInBranche;
}
# If there's still no page in node something's really wrong.
if (!$node) {
WebGUI::ErrorHandler::fatalError("Page is not in database! pageId: $pageId");
}
return $node;
}
#-------------------------------------------------------------------
=head2 move( newMother )
Moves a page to another page (ie. makes the page you execute this method on a child of newMother).
Returns 1 if the move was succesfull, 0 otherwise.
=over
=item newMother
The page under which the current page should be moved. This should be an WebGUI::Page object.
=back
=cut
sub move{
my ($self, $clipboard, $parentId, $newSequenceNumber, @newSisters, $newMother);
($self, $newMother) = @_;
# Avoid cyclic pages. Not doing this will allow people to freeze your computer, by generating infinite loops.
return 0 if (isIn($self->get("pageId"), map {$_->get("pageId")} $newMother->ancestors));
# Make sure a page is not moved to itself.
return 0 if ($self->get("pageId") == $newMother->get("pageId"));
$parentId = $self->get("parentId");
# Lower the sequence numbers of the following sisters
foreach ($self->right_sisters) {
$_->setWithoutRecache({
sequenceNumber => $_->get('sequenceNumber') - 1
});
}
# Derive new sequenceNumber
@newSisters = $newMother->daughters;
$newSequenceNumber = 1;
$newSequenceNumber = $newSisters[scalar(@newSisters)-1]->get('sequenceNumber') +1 if (@newSisters);
# Do the move
$self->unlink_from_mother;
$newMother->add_daughter($self);
$self->set({
parentId => $newMother->get("pageId"),
sequenceNumber => $newSequenceNumber,
});
return 1;
}
#-------------------------------------------------------------------
=head2 paste( newMother )
Pastes a page under newMother.
=over
=item newMother
The page under which the current page should be pasted. This should be an WebGUI::Page object.
=back
=cut
sub paste{
my ($self, $newMother);
($self, $newMother) = @_;
return $self if ($self->get("pageId") == $newMother->get("pageId"));
return WebGUI::ErrorHandler::fatalError("You cannot paste a page that's not on the clipboard. parentId:".
$self->get("parentId").", pageId:".$self->get("pageId")) unless ($self->get("parentId") == 2);
# Place page in clipboard (pageId 2)
if ($self->move($newMother)) {
$self->set({
bufferUserId => 'NULL',
bufferDate => 'NULL',
bufferPrevId => 'NULL'
});
}
return $self;
}
#-------------------------------------------------------------------
=head2 purge
This purges this object from the tree and the database.
=back
=cut
sub purge {
my ($self, $currentPage, @pagesToPurge);
$self = shift;
# Never ever modify the tree hierarchy in a walk_down...
$self->walk_down({
callback => sub {
$currentPage = shift;
push(@pagesToPurge, $currentPage);
}
});
# ... do it afterwards.
foreach $currentPage (@pagesToPurge) {
$currentPage->SUPER::delete;
}
return "";
}
#-------------------------------------------------------------------
=head2 recachePageTree
This method fetches all pageroots from the database, builds their underlying trees and caches them. Additionally
this method creates a lookup table that connects a node to a root. This is done to avoid searching through all
page tree to find a specific node.
It should only be nescesarry to call this method when setWithoutRecache is used.
=cut
sub recachePageTree {
my ($forrest, @pageRoots, $currentTree, $serializer, $node, $serialized, $cache, %pageLookup);
# Purge the cached page trees.
$cache = WebGUI::Cache->new("", "PageTree-".$session{config}{configFile});
$cache->deleteByRegex(".*");
# Fetch the complete forrest, which is actually all the pagetrees connected by a dummy root.
$forrest = WebGUI::Page->getTree();
# Cache complete forrest.
$cache = WebGUI::Cache->new('root-0','PageTree-'.$session{config}{configFile});
$cache->setDataStructure($forrest);
return "";
}
#-------------------------------------------------------------------
=head2 set ( [ data ] )
If data is given, invoking this method will set the object to the state given in data. If called without any arguments
the state of the tree is saved to the database.
A thing to note here is that the cached version of the pagetree is always refreshed after saving to the database. Refreshing
the cache is pretty expensive, so if you have to do many sets in a row, like when recursively changing style of a (sub)tree,
it would be best to use setWithoutRecache, and call recachePageTree afterwards.
=over
=item data
The properties you want to set. This parameter is optional and should be a hashref of the form {propertyA => valueA, propertyB => valueB, etc...}
=back
=cut
sub set {
my $self = shift;
my $output = $self->SUPER::set(@_);
# The very only reason we overload the set method is to make sure that the cache also gets
# updated. So let's do just that ;). This might be overkill for some situations where only
# one tree needs to be updated, but doing all of them, sure is a lot safer. (and easier!)
recachePageTree();
return $output;
}
#-------------------------------------------------------------------
=head2 setWithoutRecache ([data])
See set. The only difference with set is that the cached version of the pagetree is not updated. This means that you must
update it manually by invoking recachePageTree afterwards.
=over
=item data
The properties you want to set. This parameter is optional and should be a hashref of the form {propertyA => valueA, propertyB => valueB, etc...}
=back
=cut
sub setWithoutRecache {
my $self = shift;
return $self->SUPER::set(@_);
}
1;