diff --git a/lib/WebGUI/Operation/Page.pm b/lib/WebGUI/Operation/Page.pm index 05b4368dc..320b3ba2f 100644 --- a/lib/WebGUI/Operation/Page.pm +++ b/lib/WebGUI/Operation/Page.pm @@ -45,38 +45,36 @@ sub _changeWobjectPrivileges { } } } - -#------------------------------------------------------------------- -sub _recursivelyChangePrivileges { - my ($sth, $pageId); - $sth = WebGUI::SQL->read("select pageId from page where parentId=$_[0]"); - _changeWobjectPrivileges($_[0]) unless $session{form}{wobjectPrivileges}; - while (($pageId) = $sth->array) { - if (WebGUI::Privilege::canEditPage($pageId)) { - WebGUI::SQL->write("update page set startDate=".WebGUI::FormProcessor::dateTime("startDate").", - endDate=".WebGUI::FormProcessor::dateTime("endDate").", - ownerId=$session{form}{ownerId}, groupIdView=$session{form}{groupIdView}, - groupIdEdit=$session{form}{groupIdEdit}, wobjectPrivileges=$session{form}{wobjectPrivileges} where pageId=$pageId"); - _recursivelyChangePrivileges($pageId); - } - } - $sth->finish; -} #------------------------------------------------------------------- -sub _recursivelyChangeStyle { - my ($sth, $pageId); - $sth = WebGUI::SQL->read("select pageId from page where parentId=$_[0]"); - while (($pageId) = $sth->array) { - if (WebGUI::Privilege::canEditPage($pageId)) { - WebGUI::SQL->write("update page set styleId=$session{form}{styleId}, printableStyleId=$session{form}{printableStyleId} where pageId=$pageId"); - _recursivelyChangeStyle($pageId); +# This combines _recusivelyChangePrivileges and _recusivelyChangeStyle, since there's no use in walking down a tree twice. +sub _recursivelyChangeProperties { + my $page = shift; + + _changeWobjectPrivileges($page->get("pageId")) unless $session{form}{wobjectPrivileges}; + + $page->walk_down({ + callback => sub { + if (WebGUI::Privilege::canEditPage($_[0]->get('pageId'))) { + $_[0]->setWithoutRecache({ + startDate => WebGUI::FormProcessor::dateTime("startDate"), + endDate => WebGUI::FormProcessor::dateTime("endDate"), + ownerId => $session{form}{ownerId}, + groupIdView => $session{form}{groupIdView}, + groupIdEdit => $session{form}{groupIdEdit} + }) if ($session{form}{recursePrivs}); + $_[0]->setWithoutRecache({ + styleId => $session{form}{styleId} + }) if ($session{form}{recurseStyle}); + } + return 1; } - } - $sth->finish; + }); + + WebGUI::Page->recachePageTree; } -#------------------------------------------------------------------- +------------------------------------------------------------------- sub _reorderPages { my ($sth, $i, $pid); $sth = WebGUI::SQL->read("select pageId from page where parentId=$_[0] order by sequenceNumber"); @@ -192,16 +190,13 @@ sub _traversePageTree { #------------------------------------------------------------------- sub www_cutPage { + my ($page); if ($session{page}{pageId} < 26 && $session{page}{pageId} >= 0) { return WebGUI::Privilege::vitalComponent(); + } elsif (WebGUI::Privilege::canEditPage()) { - WebGUI::SQL->write("update page set parentId=2, " - ."bufferUserId=".$session{user}{userId}.", " - ."bufferDate=".time().", " - ."bufferPrevId=".$session{page}{parentId}." " - ."where pageId=".$session{page}{pageId}); - _reorderPages($session{page}{parentId}); - WebGUI::Session::refreshPageInfo($session{page}{parentId}); + $page = WebGUI::Page->getPage($session{page}{pageId}); + $page->cut; return ""; } else { return WebGUI::Privilege::insufficient(); @@ -484,51 +479,54 @@ sub www_editPage { #------------------------------------------------------------------- sub www_editPageSave { - my ($nextSeq, $pageId); + my ($pageId, $currentPage, $page); + if ($session{form}{pageId} eq "new") { $pageId = $session{form}{parentId}; } else { + $page = WebGUI::Page->getPage($session{form}{pageId}); $pageId = $session{form}{pageId}; } - return WebGUI::Privilege::insufficient() unless (WebGUI::Privilege::canEditPage($pageId)); + + return WebGUI::Privilege::insufficient() unless (WebGUI::Privilege::canEditPage($pageId)); + if ($session{form}{pageId} eq "new") { - ($nextSeq) = WebGUI::SQL->quickArray("select max(sequenceNumber) from page where parentId=$session{form}{parentId}"); - $nextSeq++; - $session{form}{pageId} = getNextId("pageId"); - WebGUI::SQL->write("insert into page (pageId,sequenceNumber,parentId) - values ($session{form}{pageId},$nextSeq,$session{form}{parentId})"); + $currentPage = WebGUI::Page->getPage($session{page}{pageId}); + $page = $currentPage->add; } + $session{form}{title} = "no title" if ($session{form}{title} eq ""); $session{form}{menuTitle} = $session{form}{title} if ($session{form}{menuTitle} eq ""); $session{form}{urlizedTitle} = $session{form}{menuTitle} if ($session{form}{urlizedTitle} eq ""); $session{form}{urlizedTitle} = WebGUI::Page::makeUnique(WebGUI::URL::urlize($session{form}{urlizedTitle}),$session{form}{pageId}); - WebGUI::SQL->write("update page set - title=".quote($session{form}{title}).", - styleId=$session{form}{styleId}, - printableStyleId=$session{form}{printableStyleId}, - ownerId=$session{form}{ownerId}, - groupIdView=$session{form}{groupIdView}, - groupIdEdit=$session{form}{groupIdEdit}, - newWindow=$session{form}{newWindow}, - wobjectPrivileges=$session{form}{wobjectPrivileges}, - hideFromNavigation=$session{form}{hideFromNavigation}, - startDate=".WebGUI::FormProcessor::dateTime("startDate").", - endDate=".WebGUI::FormProcessor::dateTime("endDate").", - cacheTimeout=".WebGUI::FormProcessor::interval("cacheTimeout").", - cacheTimeoutVisitor=".WebGUI::FormProcessor::interval("cacheTimeoutVisitor").", - metaTags=".quote($session{form}{metaTags}).", - urlizedTitle='$session{form}{urlizedTitle}', - redirectURL='$session{form}{redirectURL}', - languageId='$session{form}{languageId}', - defaultMetaTags='$session{form}{defaultMetaTags}', - templateId='$session{form}{templateId}', - menuTitle=".quote($session{form}{menuTitle}).", - synopsis=".quote($session{form}{synopsis})." - where pageId=$session{form}{pageId}"); - WebGUI::SQL->write("update wobject set templatePosition=1 where pageId=$session{form}{pageId} - and templatePosition>".WebGUI::Page::countTemplatePositions($session{form}{templateId})); - _recursivelyChangeStyle($session{form}{pageId}) if ($session{form}{recurseStyle}); - _recursivelyChangePrivileges($session{form}{pageId}) if ($session{form}{recursePrivs}); + $page->set({ + title => $session{form}{title}, + styleId => $session{form}{styleId}, + printableStyleId => $session{form}{printableStyleId}, + ownerId => $session{form}{ownerId}, + groupIdView => $session{form}{groupIdView}, + groupIdEdit => $session{form}{groupIdEdit}, + newWindow => $session{form}{newWindow}, + wobjectPrivileges => $session{form}{wobjectPrivileges}, + hideFromNavigation => $session{form}{hideFromNavigation}, + startDate => WebGUI::FormProcessor::dateTime("startDate"), + endDate => WebGUI::FormProcessor::dateTime("endDate"), + cacheTimeout => WebGUI::FormProcessor::interval("cacheTimeout"), + cacheTimeoutVisitor => WebGUI::FormProcessor::interval("cacheTimeoutVisitor"), + metaTags => $session{form}{metaTags}, + urlizedTitle => $session{form}{urlizedTitle}, + redirectURL => $session{form}{redirectURL}, + languageId => $session{form}{languageId}, + defaultMetaTags => $session{form}{defaultMetaTags}, + templateId => $session{form}{templateId}, + menuTitle => $session{form}{menuTitle}, + synopsis => $session{form}{synopsis} + }); + unless ($session{form}{pageId} == 'new') { + WebGUI::SQL->write("update wobject set templatePosition=1 where pageId=$session{form}{pageId} + and templatePosition>".WebGUI::Page::countTemplatePositions($session{form}{templateId})); + } + _recursivelyChangeProperties($page) if ($session{form}{recursePrivs} || $session{form}{recurseStyle}); if ($session{form}{proceed} eq "gotoNewPage") { WebGUI::Session::refreshPageInfo($session{form}{pageId}); } elsif ($session{form}{pageId} == $session{page}{pageId}) { @@ -599,15 +597,11 @@ sub www_moveTreePageRight { #------------------------------------------------------------------- sub www_pastePage { - return "" if ($session{page}{pageId} == $session{form}{pageId}); # don't let it paste to itself - my ($output, $nextSeq); - ($nextSeq) = WebGUI::SQL->quickArray("select max(sequenceNumber) from page where parentId=$session{page}{pageId}"); - $nextSeq += 1; + my ($currentPage, $pageToPaste); if (WebGUI::Privilege::canEditPage()) { - WebGUI::SQL->write("update page set parentId=$session{page}{pageId}, sequenceNumber='$nextSeq', " - ."bufferUserId=NULL, bufferDate=NULL, bufferPrevId=NULL " - ."where pageId=$session{form}{pageId}"); - _reorderPages($session{page}{pageId}); + $currentPage = WebGUI::Page->getPage($session{page}{pageId}); + $pageToPaste = WebGUI::Page->getPage($session{form}{pageId}); + $pageToPaste->paste($currentPage); return ""; } else { return WebGUI::Privilege::insufficient(); @@ -624,4 +618,3 @@ sub www_viewPageTree { } 1; - diff --git a/lib/WebGUI/Page.pm b/lib/WebGUI/Page.pm index dc7e34c0c..dc1821a28 100644 --- a/lib/WebGUI/Page.pm +++ b/lib/WebGUI/Page.pm @@ -26,6 +26,8 @@ use WebGUI::Session; use WebGUI::SQL; use WebGUI::Template; use WebGUI::Utility; +use WebGUI::DateTime; +use Data::Serializer; our @ISA = qw(WebGUI::Persistent::Tree); @@ -35,10 +37,15 @@ Package WebGUI::Page =head1 DESCRIPTION -This package provides utility functions for WebGUI's page system. +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); @@ -48,6 +55,20 @@ This package provides utility functions for WebGUI's page system. $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: @@ -57,36 +78,82 @@ These functions are available from this package: #------------------------------------------------------------------- sub classSettings { - return { - properties => { - pageId => { key => 1 }, - parentId => { defaultValue => 0 }, - title => { quote => 1 }, - styleId => { defaultValue => 0 }, - ownerId => { defaultValue => 0 }, - sequenceNumber => { defaultValue => 1 }, - metaTags => { quote => 1 }, - urlizedTitle => { quote => 1 }, - defaultMetaTags => { defaultValue => 0 }, - menuTitle => { quote => 1 }, - synopsis => { quote => 1 }, - templateId => { defaultValue => 1 }, - startDate => { defaultValue => 946710000 }, - endDate => { defaultValue => 2082783600 }, - redirectURL => { quote => 1 }, - userDefined1 => { quote => 1 }, - userDefined2 => { quote => 1 }, - userDefined3 => { quote => 1 }, - userDefined4 => { quote => 1 }, - userDefined5 => { quote => 1 }, - languageId => { defaultValue => 1 }, - groupIdView => { defaultValue => 3 }, - groupIdEdit => { defaultValue => 3 }, - hideFromNavigation => { defaultValue => 0 }, - }, - useDummyRoot => 1, - table => 'page' - } + 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 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; } #------------------------------------------------------------------- @@ -116,10 +183,38 @@ sub countTemplatePositions { } #------------------------------------------------------------------- +=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); + $self->get("pageId"); + + 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. +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 @@ -138,7 +233,6 @@ sub deCache { } #------------------------------------------------------------------- - =head2 drawTemplate ( templateId ) Returns an HTML string containing a small representation of the page template. @@ -243,7 +337,6 @@ sub generate { #------------------------------------------------------------------- - =head2 getTemplateList Returns a hash reference containing template ids and template titles for all the page templates available in the system. @@ -255,7 +348,6 @@ sub getTemplateList { } #------------------------------------------------------------------- - =head2 getTemplate ( [ templateId ] ) Returns an HTML template. @@ -276,7 +368,6 @@ sub getTemplate { } #------------------------------------------------------------------- - =head2 getTemplatePositions ( templateId ) Returns a hash reference containing the positions available in the specified page template. @@ -301,7 +392,6 @@ sub getTemplatePositions { } #------------------------------------------------------------------- - =head2 makeUnique ( pageURL, pageId ) Returns a unique page URL. @@ -334,5 +424,246 @@ sub makeUnique { return $url; } -1; +#------------------------------------------------------------------- +=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 ($serializer, $cache, $pageLookup, $node, $self, $class, $pageId, $tree); + ($class, $pageId) = @_; + $pageId ||= $session{page}{pageId}; + + WebGUI::ErrorHandler::fatalError("Illegal pageId: '$pageId'") unless ($pageId =~ /^-?\d+$/); + + # Only fetch from cache if cache is enabled in the config file + if ($session{config}{usePageCache}) { + # Fetch the correct pagetree from cache + $cache = WebGUI::Cache->new('pageLookup', 'PageTree-'.$session{config}{configFile}); + $pageLookup = $cache->getDataStructure; + $cache = WebGUI::Cache->new('root-'.$pageLookup->{$pageId},'PageTree-'.$session{config}{configFile}); + $tree = $cache->getDataStructure; + + unless (defined $tree) { + #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; + $tree = WebGUI::Page->getTree()->{0}; + } + # No caching + } else { + # Do it the diehard way. Just build a complete tree from the database. This will most definately work, but + # not too fast. A more elegant aproach would be overlaoding every Tree::DAG_Node method WebGUI::Page inherits + # and dynamically load the data from the db. + $tree = WebGUI::Page->getTree()->{0}; + + # Caching the tree in the session hash might be a good idea, to reduce tree building to only once per session. + # $session{page}{tree} = $tree; + } + + # Select the correct node from the tree + $tree->walk_down({ + callback => sub { + if ($_[0]->get('pageId') == $pageId) { + $node = $_[0]; + return 0; + } + return 1; + }}); + 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 = shift; + $newMother = shift; + + # 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 = shift; + $newMother = shift; + return $self if ($self->get("pageId") == $newMother->get("pageId")); + return WebGUI::ErrorHandler::fatalError("You cannot paste a page that's not on the clipboard.") 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 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(); + + @pageRoots = $forrest->{0}->daughters; + $serializer = Data::Serializer->new(serializer => 'Storable'); + + foreach $currentTree (@pageRoots) { + # Disconnect the tree from the dummy root. + $currentTree->unlink_from_mother; + + # Cache forrest per tree. + $cache = WebGUI::Cache->new('root-'.$currentTree->get('pageId'),'PageTree-'.$session{config}{configFile}); + $cache->setDataStructure($currentTree); + + # Create URL lookup table. + $currentTree->walk_down({ + callback => sub { + $_[1]->{_pageLookup}->{$_[0]->get('pageId')} = $_[1]->{_root}; + return 1; + }, + _root => $currentTree->get('pageId'), + _pageLookup => \%pageLookup + }); + } + + # Put the lookup table into cache + $cache = WebGUI::Cache->new('pageLookup','PageTree-'.$session{config}{configFile}); + $cache->setDataStructure(\%pageLookup); + + 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;