package WebGUI::Page; =head1 LEGAL ------------------------------------------------------------------- WebGUI is Copyright 2001-2004 Plain Black Corporation. ------------------------------------------------------------------- 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 HTML::Template; use strict; use Tie::IxHash; use Tie::CPHash; use WebGUI::Cache; use WebGUI::DateTime; use WebGUI::ErrorHandler; use WebGUI::Grouping; use WebGUI::HTMLForm; use WebGUI::HTTP; use WebGUI::Icon; use WebGUI::Id; use WebGUI::Macro; use WebGUI::Session; use WebGUI::SQL; use WebGUI::Style; use WebGUI::Template; use WebGUI::Utility; use DBIx::Tree::NestedSet; use WebGUI::MetaData; our @ISA = qw(DBIx::Tree::NestedSet); =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 or report on this hiearchy should be called in a object oriented context. =head1 SYNOPSIS Non OO functions use WebGUI::Page; $boolean = WebGUI::Page::canEdit(); $boolean = WebGUI::Page::canView(); $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); WebGUI::Page::deCache(); Some OO style methods use WebGUI::Page; $page = WebGUI::Page->getPage($pageId); $page = WebGUI::Page->getAnonymousRoot; $page = WebGUI::Page->getFirstDaughter($pageId); $page = WebGUI::Page->getGrandmother($pageId); $page = WebGUI::Page->getLeftSister($pageId); $page = WebGUI::Page->getMother($pageId); $page = WebGUI::Page->getRightSister($pageId); $page = WebGUI::Page->getTop($pageId); $page = WebGUI::Page->getWebGUIRoot($pageId); $page = WebGUI::Page->new($pageId); # the default constructor $page->cut; $page->delete; $page->paste($newMother); $page->move($newMother); $page->moveDown; $page->moveLeft; $page->moveRight; $page->moveUp; $page->purge; @array = $page->ancestors; @array = $page->daughters; @array = $page->descendants; @array = $page->generation; @array = $page->leaves_under; @array = $page->pedigree; @array = $page->self_and_ancesters; @array = $page->self_and_descendants; @array = $page->self_and_sisters; @array = $page->self_and_sisters_splitted; @array = $page->sisters; $boolean = $page->canMoveDown; $boolean = $page->canMoveLeft; $boolean = $page->canMoveRight; $boolean = $page->canMoveUp; $boolean = $page->hasDaughter; $page->add($otherPageObject,\%properties); $page->get("title"); $page->set(\%properties); # this automatically recaches the pagetree $page->setWithoutRecache; $page->traversePreOrder(&mappingFunction); $page->recacheNavigation or WebGUI::Page->recacheNavigation; =head1 SEE ALSO This class is a sub-class of DBIx::Tree::NestedSet, which is included in your WebGUI distribution. See that for additional details on the page tree. =head1 METHODS These functions are available from this package: =cut #------------------------------------------------------------------- sub _processWobjectFunctions { my ($wobject, $output, $proxyWobjectId, $cmd, $w); if (exists $session{form}{func} && exists $session{form}{wid}) { if ($session{form}{func} =~ /^[A-Za-z]+$/) { if ($session{form}{wid} eq "new") { $wobject = {wobjectId=>"new",namespace=>$session{form}{namespace},pageId=>$session{page}{pageId}}; } else { $wobject = WebGUI::SQL->quickHashRef("select * from wobject where wobjectId=".quote($session{form}{wid}),WebGUI::SQL->getSlave); if (${$wobject}{namespace} eq "") { WebGUI::ErrorHandler::warn("Wobject [$session{form}{wid}] appears to be missing or " ."corrupt, but was requested " ."by $session{user}{username} [$session{user}{userId}]."); $wobject = (); } } if ($wobject) { if (${$wobject}{pageId} != $session{page}{pageId}) { ($proxyWobjectId) = WebGUI::SQL->quickArray("select wobject.wobjectId from wobject,WobjectProxy where wobject.wobjectId=WobjectProxy.wobjectId and wobject.pageId=".quote($session{page}{pageId})." and WobjectProxy.proxiedWobjectId=".quote(${$wobject}{wobjectId}),WebGUI::SQL->getSlave); ${$wobject}{_WobjectProxy} = $proxyWobjectId; } unless (${$wobject}{pageId} eq $session{page}{pageId} || ${$wobject}{pageId} == 2 || ${$wobject}{pageId} == 3 || ${$wobject}{_WobjectProxy} ne "") { $output .= WebGUI::International::get(417); WebGUI::ErrorHandler::security("access wobject [".$session{form}{wid}."] on page '" .$session{page}{title}."' [".$session{page}{pageId}."]."); } else { if (WebGUI::Page::canView()) { $cmd = "WebGUI::Wobject::".${$wobject}{namespace}; my $load = "use ".$cmd; # gotta load the wobject before you can use it eval($load); WebGUI::ErrorHandler::warn("Wobject failed to compile: $cmd.".$@) if($@); $w = eval{$cmd->new($wobject)}; WebGUI::ErrorHandler::fatalError("Couldn't instanciate wobject: ${$wobject}{namespace}. Root Cause: ".$@) if($@); if ($session{form}{func} =~ /^[A-Za-z]+$/) { $cmd = "www_".$session{form}{func}; $output = eval{$w->$cmd}; WebGUI::ErrorHandler::fatalError("Wobject runtime error: ${$wobject}{namespace} / $session{form}{func}. Root cause: ".$@) if($@); } else { WebGUI::ErrorHandler::security("execute an invalid function: ".$session{form}{func}); } } else { $output = WebGUI::Privilege::noAccess(); } } } } else { WebGUI::ErrorHandler::security("execute an invalid function on wobject " .$session{form}{wid}.": ".$session{form}{func}); } } return $output; } #------------------------------------------------------------------- =head2 add Adds page to the right of the children of the object this method is invoked on. Returns the new page object. =head3 page A WebGUI::Page instance to be added to the children of the current object. =cut sub add { my ($self, $page, $newPageId); $self = shift; $newPageId = WebGUI::Id::generate(); $self->add_child_to_right( pageId =>$self->get('pageId'), provided_primary_key => $newPageId, parentId=>$self->get('pageId'), depth =>($self->get('depth') + 1), ); $self->recacheNavigation; return WebGUI::Page->new($newPageId); } #------------------------------------------------------------------- =head2 ancestors Returns an array of hashes containing the properties of the ancestors of the current node. =cut sub ancestors { my ($self); $self = shift; return @{$self->get_parents_flat( id => $self->get('pageId') )}; } #------------------------------------------------------------------- =head2 canEdit ( [ pageId ] ) Returns a boolean (0|1) value signifying that the user has the required privileges. =head3 pageId The unique identifier for the page that you wish to check the privileges on. Defaults to the current page id. =cut sub canEdit { my $pageId = shift || $session{page}{pageId}; my (%page); tie %page, 'Tie::CPHash'; if ($pageId ne $session{page}{pageId}) { %page = WebGUI::SQL->quickHash("select ownerId,groupIdEdit from page where pageId=".quote($pageId)); } else { %page = %{$session{page}}; } if ($session{user}{userId} eq $page{ownerId}) { return 1; } else { return WebGUI::Grouping::isInGroup($page{groupIdEdit}); } } #------------------------------------------------------------------- =head2 canMoveDown Returns true if the current node can be moved down the tree. Ie. can be made a child of it's left sister. =cut sub canMoveDown { my ($self) = shift; return $self->hasLeftSister; } #------------------------------------------------------------------- =head2 canMoveLeft Returns true if the current node can be moved left. Ie. if it can swap places with it's left sister. =cut sub canMoveLeft { my ($self, $mother); $self = shift; $mother = $self->getMother; return (($self->get('nestedSetLeft') - $mother->get('nestedSetLeft')) > 1); } #------------------------------------------------------------------- =head2 canMoveRight Returns true if the current node can be moved rightt. Ie. if it can swap places with it's right sister. =cut sub canMoveRight { my ($self, $mother); $self = shift; $mother = $self->getMother; return (($mother->get('nestedSetRight') - $self->get('nestedSetRight')) > 1); } #------------------------------------------------------------------- =head2 canMoveUp Returns true if the current node can be moved up the tree. Ie. if it can be made a child of it's grandmother. =cut sub canMoveUp { my ($self) = shift; return ($self->get('depth') > 0); } #------------------------------------------------------------------- =head2 canView ( [ pageId ] ) Returns a boolean (0|1) value signifying that the user has the required privileges. Always returns users that have the rights to edit this page. =head3 pageId The unique identifier for the page that you wish to check the privileges on. Defaults to the current page id. =cut sub canView { my $pageId = shift || $session{page}{pageId}; my %page; tie %page, 'Tie::CPHash'; if ($pageId eq $session{page}{pageId}) { %page = %{$session{page}}; } else { %page = WebGUI::SQL->quickHash("select ownerId,groupIdView,startDate,endDate from page where pageId=".quote($pageId),WebGUI::SQL->getSlave); } if ($session{user}{userId} eq $page{ownerId}) { return 1; } elsif ($page{startDate} < WebGUI::DateTime::time() && $page{endDate} > WebGUI::DateTime::time() && WebGUI::Grouping::isInGroup($page{groupIdView})) { return 1; } else { return canEdit($pageId); } } #------------------------------------------------------------------- =head2 countTemplatePositions ( templateId ) Returns the number of template positions in the specified page template. =head3 templateId The id of the page template you wish to count. =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. =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 daughters Returns an array of hashes containing the properties of the daughters of the current node. =cut sub daughters { my ($self); $self = shift; return @{$self->get_children_flat( id => $self->get('pageId'), depth => 1 )}; } #------------------------------------------------------------------- =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; =head3 pageId The id of the page to decache. Defaults to the current page id. =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. =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 descendants Returns an array of hashes containing the properties of the descendants of the current node. =cut sub descendants { my ($self); $self = shift; return @{$self->get_children_flat( id => $self->get('pageId') )}; } #------------------------------------------------------------------- =head2 drawTemplate ( templateId ) Returns an HTML string containing a small representation of the page template. =head3 templateId The id of the page template you wish to draw. =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/\.*?\<\/style\>//gi; $template =~ s/\.*?\<\/script\>//gi; $template =~ s/\/\/ig; $template =~ s/\.*?\<\/tmpl\_loop\>/$1/ig; $template =~ s/\.*?\<\/tmpl_if\>//ig; $template =~ s/\//ig; $template =~ s/\<\/tmpl_if\>//ig; $template =~ s/\//ig; return $template; } #------------------------------------------------------------------- =head2 generate ( ) Generates the content of the page. =cut sub generate { return WebGUI::Privilege::noAccess() unless (canView()); my $output = _processWobjectFunctions(); return $output if ($output); my %var; if ($session{page}{defaultMetaTags}) { WebGUI::Style::setMeta({'http-equiv'=>"Keywords", name=>"Keywords", content=>join(",",$session{page}{title},$session{page}{menuTitle})}); WebGUI::Style::setMeta({'http-equiv'=>"Description", name=>"Description", content=>$session{page}{synopsis}}) if ($session{page}{synopsis}); } WebGUI::Style::setRawHeadTags($session{page}{metaTags}); if ($session{page}{redirectURL} && !$session{var}{adminOn}) { WebGUI::HTTP::setRedirect(WebGUI::Macro::process($session{page}{redirectURL})); } $var{'page.canEdit'} = canEdit(); $var{'page.controls'} = pageIcon() .deleteIcon('op=deletePage') .editIcon('op=editPage') .moveUpIcon('op=movePageUp') .moveDownIcon('op=movePageDown') .cutIcon('op=cutPage'); $var{'page.controls'} .= exportIcon('op=exportPage') if defined ($session{config}{exportPath}); my $sth = WebGUI::SQL->read("select * from wobject where pageId=".quote($session{page}{pageId})." order by sequenceNumber, wobjectId",WebGUI::SQL->getSlave); 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=".quote(${$wobject}{wobjectId}),WebGUI::SQL->getSlave); if($wobjectProxy->{proxyByCriteria}) { $wobjectProxy->{proxiedWobjectId} = WebGUI::MetaData::getWobjectByCriteria($wobjectProxy) || $wobjectProxy->{proxiedWobjectId}; } $wobject = WebGUI::SQL->quickHashRef("select * from wobject where wobject.wobjectId=".quote($wobjectProxy->{proxiedWobjectId}),WebGUI::SQL->getSlave); 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 $originalWobjectPage = WebGUI::Page->new($wobject->{pageId}); $wobject->{'original.page.url'} = WebGUI::URL::gateway($originalWobjectPage->get("urlizedTitle")); } } my $cmd = "WebGUI::Wobject::".${$wobject}{namespace}; my $load = 'use '.$cmd; eval($load); WebGUI::ErrorHandler::warn("Wobject failed to compile: $cmd.".$@) if($@); 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'=>$w->canView, 'wobject.canEdit'=>$w->canEdit, '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($session{page}{templateId},"page",\%var); } #------------------------------------------------------------------- =head2 generation Returns an array of hashes containing the properties of the same generation as the current node. The current node, being a member of it's own generation, is of course included. A generation consists of all nodes with the same depth (or level) in the tree. =cut sub generation { my ($self, $sth, %row, @result); $self = shift; $sth = WebGUI::SQL->read( "select a.* from page as a, page as b where a.depth = b.depth and b.pageId = ".quote($self->get('pageId')). " order by nestedSetLeft"); while (%row = $sth->hash) { push(@result, {(%row)}); } return @result; } #------------------------------------------------------------------- =head2 get( property ) Returns a hash reference of all the page properties. =head3 property Returns a scalar containing the value of the specififed proeprty. =cut sub get { my ($self, $property) = @_; if ($property) { return $self->{_pageProperties}->{$property}; } return $self->{_pageProperties}; } #------------------------------------------------------------------- =head2 getAnonymousRoot Returns the 'ueber'-root, the root with pageId 0, the one that holds all WebGUI roots together, the node that brings the balance back into the force ;) Note that this node is only in the database because of design. You cannot put stuff on it. Well actually you can, but you don't want to. Trust me. Use it to add WebGUI roots or traverse the whole page tree instead . =cut sub getAnonymousRoot { return WebGUI::Page->new(0); } #------------------------------------------------------------------- =head2 getFirstDaughter( pageId ) Return the first (leftmost) daughter of the current node when called in instance context, returns the first daughter of 'pageId' when called in class context. =head3 pageId Only required if called in class context. The pageId of the page of which you want the daughter of. Defaults to the current page. =cut sub getFirstDaughter { my ($self, $pageId, $daughterId, @daughters); ($self, $pageId) = @_; unless (ref($self)) { $self = WebGUI::Page->new($pageId || $session{page}{pageId}); } @daughters = $self->daughters; return undef unless (scalar(@daughters)); $daughterId = $daughters[0]->{pageId}; return WebGUI::Page->new($daughterId); } #------------------------------------------------------------------- =head2 getGrandmother( pageId ) Returns the grandmother of the current node, or, when called in class context, the garndmother of 'pageId'. =head3 pageId Only required if called in class context. The pageId of the page of which you want the grandmother of. Defaults to the current page. =cut sub getGrandmother { my ($self, $pageId, $grannyId); ($self, $pageId) = @_; unless (ref($self)) { $self = WebGUI::Page->new($pageId || $session{page}{pageId}); } return undef if ($self->get('depth') < 1); # We use self and ancestors here because ancestors strips on the wrong side. $grannyId = (reverse $self->self_and_ancestors)[2]->{pageId}; return WebGUI::Page->new($grannyId); } #------------------------------------------------------------------- =head2 getLeftSister( pageId ) Returns the left sister of the current node, or, when called in class context, the left sister of 'pageId'. =head3 pageId Only required if called in class context. The pageId of the page of which you want the left sister of. Defaults to the current page. =cut sub getLeftSister { my ($self, $pageId, $leftSisterId); ($self, $pageId) = @_; unless (ref($self)) { $self = WebGUI::Page->new($pageId || $session{page}{pageId}); } ($leftSisterId) = WebGUI::SQL->quickArray("select pageId from page where nestedSetRight=".($self->get('nestedSetLeft') - 1)); return undef unless($leftSisterId); return WebGUI::Page->new($leftSisterId); } #------------------------------------------------------------------- =head2 getMother( pageId ) Returns the mother of the current node, or, when called in class context, the left sister of 'pageId'. =head3 pageId Only required if called in class context. The pageId of the page of which you want the mother of. Defaults to the current page. =cut sub getMother { my ($self, $pageId, $mommyId); ($self, $pageId) = @_; unless (ref($self)) { $self = WebGUI::Page->new($pageId || $session{page}{pageId}); } return undef if ($self->get('depth') < 0); # We use self and ancestors here because ancestors strips on the wrong side. $mommyId = (reverse $self->self_and_ancestors)[1]->{pageId}; return WebGUI::Page->new($mommyId); } #------------------------------------------------------------------- =head2 getPage( pageId ) Returns the page identified by 'pageId'. =head3 pageId The pageId of the page you want. Defaults to the current page. =cut sub getPage { my ($pageId); $pageId = $session{page}{pageId}; $pageId = $_[1] if (defined $_[1]); return WebGUI::Page->new($pageId); } #------------------------------------------------------------------- =head2 getRightSister( pageId ) Returns the right sister of the current node, or, when called in class context, the right sister of 'pageId'. =head3 pageId Only required if called in class context. The pageId of the page of which you want the right sister of. Defaults to the current page. =cut sub getRightSister { my ($self,$pageId, $rightSisterId); ($self, $pageId) = @_; unless (ref($self)) { $self = WebGUI::Page->new($pageId || $session{page}{pageId}); } ($rightSisterId) = WebGUI::SQL->quickArray("select pageId from page where nestedSetLeft=".($self->get('nestedSetRight') + 1)); return undef unless(defined $rightSisterId); return WebGUI::Page->new($rightSisterId); } #------------------------------------------------------------------- =head2 getTop( pageId ) Returns the top page (child of a WebGUI root, depth = 1) of the current node, or, when called in class context, the top of 'pageId'. =head3 pageId Only required if called in class context. The pageId of the page of which you want the top page of. Defaults to the current page. =cut sub getTop { my ($self, $pageId, $topId); ($self, $pageId) = shift; unless (ref($self)) { $self= WebGUI::Page->new($pageId || $session{page}{pageId}); } if ($self->get('depth') == 1) { $topId = $self->get('pageId'); #The current page is a top level page } elsif ($self->get('depth') > 1) { $topId = ($self->self_and_ancestors)[2]->{pageId}; } else { $topId = ($self->daughters)[0]->{pageId}; } return WebGUI::Page->new($topId); } #------------------------------------------------------------------- =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. =head3 templateId The id of the page template you wish to retrieve. Defaults to the current page's template id. =cut sub getTemplate { my $templateId = shift || $session{page}{templateId}; my $template = WebGUI::Template::get($templateId,"page"); return $template->{template}; } #------------------------------------------------------------------- =head2 getTemplatePositions ( templateId ) Returns a hash reference containing the positions available in the specified page template. =head3 templateId The id of the page template you wish to retrieve the positions from. =cut sub getTemplatePositions { my (%hash, $template, $i); tie %hash, "Tie::IxHash"; for ($i=1; $i<=countTemplatePositions($_[0]); $i++) { $hash{$i} = $i; } return \%hash; } #------------------------------------------------------------------- =head2 getWebGUIRoot( pageId ) Returns the WebGUI root (depth = 0) of the current node, or, when called in class context, the WebGUI root of 'pageId'. =head3 pageId Only required if called in class context. The pageId of the page of which you want the WebGUI root of. Defaults to the current page. =cut sub getWebGUIRoot { my ($self, $pageId, $rootId); ($self, $pageId) = shift; unless (ref($self)) { $self= WebGUI::Page->new($pageId || $session{page}{pageId}); } if ($self->get('depth') == 0) { #The current page is a WebGUI root $rootId = $self->get('pageId'); } elsif ($self->get('depth') > 0) { $rootId = ($self->ancestors)[1]->{pageId}; } else { #There's no root, your tree is broken return undef; } return WebGUI::Page->new($rootId); } #------------------------------------------------------------------- =head2 hasDaughter Returns true if the page has one or more daughters =cut sub hasDaughter { my ($self) = shift; return ($self->get('nestedSetRight') - $self->get('nestedSetLeft') > 1); } #------------------------------------------------------------------- =head2 leaves_under Returns an array of hashes containing the properties of all leaves (pages without children) under the page =cut sub leaves_under { my ($self, $sth, %row, @result); $self = shift; $sth = WebGUI::SQL->read( "select a.* from page as a, page as b where (a.nestedSetLeft between b.nestedSetLeft and b.nestedSetRight) and (a.nestedSetRight = a.nestedSetLeft + 1) b.pageId = ".quote($self->get('pageId')). " order by nestedSetLeft"); while (%row = $sth->hash) { push(@result, {(%row)}); } return @result; } #------------------------------------------------------------------- =head2 makeUnique ( pageURL, pageId ) Returns a unique page URL. =head3 url The URL you're hoping for. =head3 pageId The page id of the page you're creating a URL for. =cut sub makeUnique { my $url = $_[0] || "_1"; my $pageId = $_[1] || "new"; my $where; unless ($pageId eq "new") { $where .= " and pageId<>".quote($pageId); } my ($test) = WebGUI::SQL->quickArray("select urlizedTitle from page where urlizedTitle=".quote($url).$where); if ($test) { my @parts = split(/\./,$url); if ($parts[0] =~ /(.*)(\d+$)/) { $parts[0] = $1.($2+1); } elsif ($test ne "") { $parts[0] .= "2"; } $url = join(".",@parts); $url = makeUnique($url,$pageId); } return $url; } #------------------------------------------------------------------- =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. =head3 newMother The page under which the current page should be moved. This should be an WebGUI::Page object. =cut sub move{ my ($self, $newMother, $parentId, $diff, $diff2, $sql, $depthDiff, $between, $updateRange, $moveNextToMother); ($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 {$_->{pageId}} $newMother->ancestors)); # Make sure a page is not moved to itself. return 0 if ($self->get('pageId') eq $newMother->get("pageId")); # Make sure a page is not moved to it's own mother return 0 if ($self->get('parentId') eq $newMother->get('pageId')); $parentId = $self->get("parentId"); # We move to the right of the children of $newMother. $depthDiff = $self->get('depth') - $newMother->get('depth') - 1; # It is important if the page moves 'up' or 'down' in nestedSetLeft and nestedSetRight value if ($self->get('nestedSetLeft') < $newMother->get('nestedSetLeft')) { $between = ($self->get('nestedSetRight') + 1)." and ".($newMother->get('nestedSetRight') - 1); $updateRange = $self->get('nestedSetLeft')." and ".$newMother->get('nestedSetRight'); $diff = $self->get('nestedSetRight') - $self->get('nestedSetLeft') + 1; $diff2 = $newMother->get('nestedSetRight') - $self->get('nestedSetRight') - 1; } else { $between = $newMother->get('nestedSetRight')." and ".($self->get('nestedSetLeft') - 1); $updateRange = $newMother->get('nestedSetLeft')." and ".($self->get('nestedSetRight')+1); $diff = $self->get('nestedSetLeft') - $self->get('nestedSetRight') - 1; $diff2 = $newMother->get('nestedSetRight') - $self->get('nestedSetLeft'); } # Set the new depth WebGUI::SQL->write("update page set depth=depth - $depthDiff where nestedSetLeft between ".$self->get('nestedSetLeft')." and ".$self->get('nestedSetRight')); # Do the magic: cast move on tree $sql = " update page set nestedSetLeft = case when nestedSetLeft between ". $self->get('nestedSetLeft')." and ".$self->get('nestedSetRight')." then nestedSetLeft + $diff2 when nestedSetLeft between ". $between ." then nestedSetLeft - $diff else nestedSetLeft end, nestedSetRight = case when nestedSetRight between ". $self->get('nestedSetLeft') ." and ". $self->get('nestedSetRight') ." then nestedSetRight + $diff2 when nestedSetRight between ". $between ." then nestedSetRight - $diff else nestedSetRight end where nestedSetRight between $updateRange or nestedSetLeft between $updateRange"; WebGUI::SQL->write($sql); # Set the parentId to the right node. WebGUI::SQL->write("update page set parentId=".quote($newMother->get('pageId'))." where pageId=".quote($self->get('pageId'))); WebGUI::Page->recacheNavigation; return 1; } #------------------------------------------------------------------- =head2 moveDown Moves the page down the tree. Ie. makes the page a daughter of it's left sister. =cut sub moveDown { my ($self, $leftSister); $self = shift; $leftSister = $self->getLeftSister; return 0 unless (defined $leftSister); $self->move($leftSister); return 1; } #------------------------------------------------------------------- =head2 moveLeft Move the page to the left. Ie. swaps places with it's left sister. =cut sub moveLeft { my ($self, $leftSister); $self = shift; $leftSister = $self->getLeftSister; return 0 unless (defined $leftSister); $self->swap_nodes( first_id => $self->get('pageId'), second_id => $leftSister->get('pageId') ); WebGUI::Page->recacheNavigation; return 1; } #------------------------------------------------------------------- =head2 moveRight Move the page to the right. Ie. swaps places with it's right sister. =cut sub moveRight { my ($self, $rightSister); $self = shift; $rightSister = $self->getRightSister; return 0 unless (defined $rightSister); $self->swap_nodes( first_id => $self->get('pageId'), second_id => $rightSister->get('pageId') ); WebGUI::Page->recacheNavigation; return 1; } #------------------------------------------------------------------- =head2 moveUp Moves the page up the tree. Ie. makes the page the right sister of it's mother. =cut sub moveUp { my ($self, $mother, $diff, $diff2, $sql); $self = shift; $mother = $self->getMother; # Don't move to an nonexistent node; return 0 if (!defined $mother); # Don't allow to move up if node is already a webguiroot; return 0 if ($mother->get('pageId') =~ /^\d+$/ && $mother->get('pageId') == 0); # Update depth, we do this before the move because now we know the nestedSetRight range of nodes # that change in depth. WebGUI::SQL->write("update page set depth=depth-1 where nestedSetRight between ".$self->get('nestedSetLeft')." and ".$self->get('nestedSetRight')); # Do some movement magic! $diff = $self->get('nestedSetRight') - $self->get('nestedSetLeft') + 1; $diff2 = $mother->get('nestedSetRight') - $self->get('nestedSetRight'); $sql = " update page set nestedSetLeft = case when nestedSetLeft between ". $self->get('nestedSetLeft')." and ".$self->get('nestedSetRight')." then nestedSetLeft + $diff2 when nestedSetLeft between ". ($self->get('nestedSetRight') + 1) ." and ". $mother->get('nestedSetRight') ." then nestedSetLeft - $diff else nestedSetLeft end, nestedSetRight = case when nestedSetRight between ". $self->get('nestedSetLeft') ." and ". $self->get('nestedSetRight') ." then nestedSetRight + $diff2 when nestedSetRight between ". ($self->get('nestedSetRight') + 1) ." and ". $mother->get('nestedSetRight') ." then nestedSetRight - $diff else nestedSetRight end, parentId = case pageId when ". quote($self->get('pageId')) ." then ". quote($mother->get('parentId'))." else parentId end where nestedSetRight between ". $self->get('nestedSetLeft') ." and ". $mother->get('nestedSetRight')." or nestedSetLeft between ". $self->get('nestedSetLeft') ." and ". $mother->get('nestedSetRight'); WebGUI::SQL->write($sql); WebGUI::Page->recacheNavigation; return 1; } #------------------------------------------------------------------- =head2 new ( pageId || { properties } ) Creates a new page object. You can't create pages in the database with this, though. Use add instead. If called without arguments it' fetches the current page (the one in $session{page}{pageId}) from the database and returns an WebGUI::Page object of it. You can pass one argument. This can be either a pageId of another page than the current you want, or a hashref containing page properties. You can use the latter if you already have page properties (returned by ancestors or something like it for example), and save a (redundant) database query. You can of course also use it to fool the system with dummy pages and do all kinds of magic that I can't imagine with it. =head3 pageId || { properties } You can pass either a pageId or a properties hashref. See above for an explanation =cut sub new { my ($class, $self, $properties); ($class, $properties) = @_; $self = $_[0]->SUPER::new( table_name => 'page', left_column_name => 'nestedSetLeft', right_column_name => 'nestedSetRight', id_name => 'pageId', dbh => $session{dbh}, no_alter_table => 1, no_locking => 1, no_id_creation => 1, ); unless (ref($properties)) { $properties = WebGUI::SQL->quickHashRef("select * from page where pageId=".quote($_[1])); } return undef unless (defined $properties->{pageId}); $self->{_pageProperties} = $properties; return $self; } #------------------------------------------------------------------- =head2 paste( newMother ) Pastes a page under newMother. =head3 newMother The page under which the current page should be pasted. This should be an WebGUI::Page object. =cut sub paste{ my ($self, $newMother); ($self, $newMother) = @_; # You do not want to paste a page onto itself, believe me. 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 pedigree Ok, this does something funky. It returns an array of hashes containing page properties of the mothers of the page and their daughter, the page itself and it's daugthers. It used for the flexmenu. =cut sub pedigree { my ($self, $leftSisters, $currentPage, $rightSisters, @flexMenu, $node); $self = shift; ($leftSisters, $currentPage, $rightSisters) = $self->self_and_sisters_splitted; @flexMenu = (@{$leftSisters}, {%{$currentPage}}, $self->daughters, @{$rightSisters}); while (defined($self=$self->getMother) && ref($self)) { ($leftSisters, $currentPage, $rightSisters) = $self->self_and_sisters_splitted; @flexMenu = (@{$leftSisters}, {%{$currentPage}}, @flexMenu, @{$rightSisters}); } return @flexMenu; } #------------------------------------------------------------------- =head2 purge This purges this object and all it's children from the tree and the database. =cut sub purge { my ($self); $self = shift; $self->delete_self_and_children( id => $self->get('pageId') ); WebGUI::Page->recacheNavigation; return ""; } #------------------------------------------------------------------- =head2 recacheNavigation Actually this doesn't recache anything, but it might be in the future. Hence the name. Currently it purges all Navigation cache objects. You should call it if you changed the pagetree. Note that the methods in this module that modify the tree already call this. If you only change some navigation properties of a navigation element, you should use a more restricted cache purge. =cut sub recacheNavigation { WebGUI::Cache->new("", "Navigation-".$session{config}{configFile})->deleteByRegex(".*"); return ""; } #------------------------------------------------------------------- =head2 self_and_ancestors Returns an array of hashrefs containing the page properties of this node and it's ancestors. =cut sub self_and_ancestors { my ($self); $self = shift; return @{$self->get_self_and_parents_flat( id => $self->get('pageId') )}; } #------------------------------------------------------------------- =head2 self_and_descendants Returns an array of hashrefs containing the page properties of this node and it's descendants. =cut sub self_and_descendants { my ($self); $self = shift; my @options = @_; return @{$self->get_self_and_children_flat( id => $self->get('pageId'), @options )}; } #------------------------------------------------------------------- =head2 self_and_sisters Returns an array of hashrefs containing the page properties of this node and it's sisters. =cut sub self_and_sisters { my ($self, $sth, %row, @result); $self = shift; tie %row,'Tie::CPHash'; $sth = WebGUI::SQL->read( "select a.* from page as a, page as b where a.parentId = b.parentId and b.pageId = ".quote($self->get('pageId')). " order by nestedSetLeft"); while (%row = $sth->hash) { push(@result, {(%row)}); } return @result; } #------------------------------------------------------------------- =head2 self_and_sisters_splitted Returns an array with the following contents: - [ leftSisters ] an arrayref of hashref containing the properties of the left sisters of the page. - $currentPage an hashref containing the page properties of this node - [ rightsister ] an arrayref of hashref containing the properties of the right sisters of the page. =cut sub self_and_sisters_splitted { my ($self, $haveAllLeftSisters, $currentPage, @leftSisters, @rightSisters); $self = shift; $haveAllLeftSisters = 0; foreach ($self->self_and_sisters) { if ($_->{pageId} eq $self->get('pageId')) { $currentPage = $_; $haveAllLeftSisters = 1; } elsif ($haveAllLeftSisters) { push (@rightSisters, $_); } else { push (@leftSisters, $_); } } return (\@leftSisters, $currentPage, \@rightSisters); } #------------------------------------------------------------------- =head2 sisters Returns an array of hashrefs containing the page properties of this nodes sisters. The node not included. =cut sub sisters { my ($self, $sth, %row, @result); $self = shift; $sth = WebGUI::SQL->read( "select a.* from page as a, page as b where a.pageId !=".quote($self->get('pageId'))." and a.parentId = b.parentId and b.pageId = ".quote($self->get('pageId')). " order by nestedSetLeft"); while (%row = $sth->hash) { push(@result, {(%row)}); } return @result; } #------------------------------------------------------------------- =head2 set ( { properties } ) 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. This method purges the Navigation cache. Note that if you have to save a lot of properties in row, it's better to use setWithoutRecache, and call recacheNavigation manually. This saves some time. =head3 properties The properties you want to set. This parameter is optional and should be a hashref of the form {propertyA => valueA, propertyB => valueB, etc...} =cut sub set { my ($self, $properties); ($self, $properties) = @_; $self->setWithoutRecache($properties); WebGUI::Page->recacheNavigation; return ""; } #------------------------------------------------------------------- =head2 setWithoutRecache ( { properties } ) 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. =head3 properties The properties you want to set. This parameter is optional and should be a hashref of the form {propertyA => valueA, propertyB => valueB, etc...} =cut sub setWithoutRecache { my ($self, $properties); ($self, $properties) = @_; $properties = $self->{_properties} unless ($properties); if (scalar(keys(%{$properties}))) { WebGUI::SQL->write("update page set ".join(', ', map {"$_=".quote($properties->{$_})} keys %{$properties})." where pageId=".quote($self->get('pageId'))); } return ""; } #------------------------------------------------------------------- =head2 traversePreOrder ( &mappingFunction ) Traverses the tree from this node down in pre-order fashion and excutes (maps) the mapping function onto each node. Also maps onto this node except if it is the anonymous root. This has some but very limited compatibility with the callback property of the walk_down method of Tree::DAG_Node. =head3 mappingFunction This should be a coderef pointing to your mapping function. The arguments that are passed to this function are a page object and a hashref containing only _depth for now. =cut sub traversePreOrder { my ($self, $mappingFunction, $initialDepth, $page, @pages); ($self, $mappingFunction, $initialDepth) = @_; @pages = $self->self_and_descendants; # The 'ueber'-root contains no data so we do not want to return i! shift @pages if ($pages[0]->{'pageId'} == 0); foreach (@pages) { $page = WebGUI::Page->new($_->{'pageId'}); &$mappingFunction($page, {_depth=>$page->get('depth')}); } return @pages; } 1;