From 684ce5a7ca8f3c8162f0b5e6991ae81c06e76de6 Mon Sep 17 00:00:00 2001 From: Chris Nehren Date: Wed, 16 Apr 2008 22:16:11 +0000 Subject: [PATCH] The export system has been completely rewritten to be less monolithic and easier to understand. It's also got a fully rewritten test suite (145 and counting). It uses Path::Class for much of its work, which is now a requirement for wG. --- docs/changelog/7.x.x.txt | 2 + docs/gotcha.txt | 6 + lib/WebGUI/AssetExportHtml.pm | 1018 +++++++++++----- .../Activity/ExportVersionTagToHtml.pm | 4 +- sbin/testEnvironment.pl | 1 + t/Asset/AssetExportHtml.t | 1019 ++++++++++++++++- 6 files changed, 1710 insertions(+), 340 deletions(-) diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 042d6f34e..2e2ba4f97 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -4,6 +4,8 @@ - fix: Event is no longer editable by anyone who can add events - fixed: Package search is slow for large websites - fixed: rich editor image picker displays incorrectly in IE + - fixed: the export system was largely incomprehensible. rewritten. + - the new export system now needs Path::Class 7.5.10 - fix: Syntax error in GetCsMail diff --git a/docs/gotcha.txt b/docs/gotcha.txt index 4ce6267b8..7f779c876 100644 --- a/docs/gotcha.txt +++ b/docs/gotcha.txt @@ -8,6 +8,12 @@ versions. Be sure to heed the warnings contained herein as they will save you many hours of grief. +7.5.11 +-------------------------------------------------------------------- + * The exporting system before this release was largely incomprehensible and + difficult to understand. It has been rewritten, and now requires Path::Class to + function. + 7.5.9 -------------------------------------------------------------------- * WebGUI 7.5.6 uses a Unicode database connection, but this can cause problems diff --git a/lib/WebGUI/AssetExportHtml.pm b/lib/WebGUI/AssetExportHtml.pm index 5893d978f..0914caaec 100644 --- a/lib/WebGUI/AssetExportHtml.pm +++ b/lib/WebGUI/AssetExportHtml.pm @@ -15,8 +15,15 @@ package WebGUI::Asset; =cut use strict; +use File::Basename; use File::Path; use FileHandle; +use Path::Class; +use Scalar::Util 'looks_like_number'; +use WebGUI::International; +use WebGUI::Exception; +use WebGUI::Session; +use URI::URL; =head1 NAME @@ -39,204 +46,422 @@ These methods are available from this class: #------------------------------------------------------------------- -=head2 checkExportPath ( ) +=head2 exportCheckPath ( session ) -Returns a descriptive error message (HTML) if the export path is not -writable, does not exist, or is not specified in the per-domain WebGUI -config file. +Class method. Tries very hard to ensure that exportPath is defined in the +configuration file, that it exists on the filesystem (creating any directories +necessary), and that the OS user running WebGUI can write to it. Throws an +appropriate exception on failure and returns a true value on success. + +Takes the following parameters: + +=over 4 + +=item session + +A reference to a L object. + +Throws the following exceptions: + +=over 4 + +=item WebGUI::Error::InvalidParam + +exportPath isn't defined in the configuration file. + +=item WebGUI::Error + +Encountered filesystem permission problems with the defined exportPath + +=back 4 =cut -sub checkExportPath { - my $self = shift; - my $error; - if (defined $self->session->config->get("exportPath")) { - if (-d $self->session->config->get("exportPath")) { - unless (-w $self->session->config->get("exportPath")) { - $error .= 'Error: The export path '.$self->session->config->get("exportPath").' is not writable.
- Make sure that the webserver has permissions to write to that directory'; - } - } else { - eval {mkpath($self->session->config->get("exportPath"),0)}; - if ($@) { - $error .= 'Error: The export path '.$self->session->config->get("exportPath").' does not exist, and couldn\'t create it because '.$@; - } - } - } else { - $error.= 'Error: The export path is not configured. Please set the exportPath variable in the WebGUI config file'; - } - $error = '

'.$error.'

' if $error; - return $error; -} +sub exportCheckPath { + my $class = shift; + my $session = shift; -#------------------------------------------------------------------- + # make sure we were given the right parameters + if(ref $session ne 'WebGUI::Session') { + WebGUI::Error::InvalidObject->throw(error => "first param to exportCheckPath must be a WebGUI::Session"); + } -# Private method to do most of the work for exporting. Returns -# ($success_flag, $description). + my $exportPath = $session->config->get('exportPath'); -# Buggo: probably shouldn't be doing i18n stuff here; refactor this -# further + # first check that the path is defined in the config file and that it's not + # an empty string + if(!defined $exportPath || !$exportPath) { + WebGUI::Error::InvalidObject->throw(error => 'exportPath must be defined and not ""'); + } -sub _exportAsHtml { - my $self = shift; - my $quiet = shift; - my $userId = shift; - my $index = shift; - my $extrasUploadsAction = shift; - my $rootUrlAction = shift; - my $startTime = $self->session->datetime->time(); - - my $exportPathError = $self->checkExportPath(); - if ($exportPathError) { - return (0, $exportPathError); - } - - my $exportPath = $self->session->config->get('exportPath'); - my $defaultAssetId = $self->session->setting->get('defaultPage'); - my $defaultAssetPath = undef; - - my $i18n = WebGUI::International->new($self->session, 'Asset'); - - # Get a list of the asset IDs we need, reverse sorted by URL - my $tempSession = WebGUI::Session->open($self->session->config->getWebguiRoot, $self->session->config->getFilename); - $tempSession->user({userId=>$userId}); - my $newSelf = WebGUI::Asset->new($tempSession, $self->getId, $self->get("className"), $self->get("revisionDate")); - my $assetIds = $newSelf->getLineage(["self","descendants"],{ - endingLineageLength => $newSelf->getLineageLength+$self->session->form->process("depth"), - orderByClause => 'assetData.url DESC', - }); - $tempSession->var->end; - $tempSession->close; - - # We're going to walk up the URL branch, making the deepest paths first - my $exportedCount = 0; - ASSET: foreach my $assetId (@{$assetIds}) { - my $assetSession = WebGUI::Session->open($self->session->config->getWebguiRoot, $self->session->config->getFilename); - $assetSession->user({userId=>$userId}); - my $asset = WebGUI::Asset->newByDynamicClass($assetSession, $assetId); - my $url = $asset->get("url"); - - # notify we can't output because user selected can't view the page - unless ($asset->canView($userId)) { - $self->session->output->print(sprintf($i18n->get('bad user privileges')."\n") . $asset->getUrl) unless $quiet; - $assetSession->var->end; - $assetSession->close; - next; - } - - # find out where we're exporting - my $pathData = $self->_translateUrlToPath($url, $index); - if (my $error = $pathData->{'error'}) { - return (0, $error); - } - my $path = $exportPath . '/'. $pathData->{'path'}; - my $filename = $pathData->{'filename'}; - my $pathWithFilename = $path.'/'.$filename; - $pathWithFilename =~ s{//}{/}g; - - # if this asset isn't exportable, skip it. - my $parentAssets = $asset->getLineage(['ancestors'], { returnObjects => 1} ); - for my $exportCheck(@{$parentAssets}, $asset) { - # don't count the root asset - next if $exportCheck->getUrl eq '/root'; - unless ($exportCheck->get('isExportable')) { - $self->session->output->print("$pathWithFilename skipped, not exportable
") unless $quiet; - $assetSession->var->end; - $assetSession->close; - next ASSET; - } + # now that we know that it's defined and not an empty string, test if it exists. + if(!-e $exportPath) { + # it doesn't exist; let's try making it + eval { mkpath( [$exportPath] ) }; + if($@) { + WebGUI::Error->throw(error => "can't create exportPath $exportPath"); } - # this is needed for symlinking - if ($asset->getId eq $defaultAssetId) { - $defaultAssetPath = (length($pathData->{'path'}) ? $pathData->{'path'}."/" : "") . $pathData->{'filename'}; - } + # the path didn't exist, and we succeeded creating it. Therefore we know we can write to it and that it's an actual directory. Nothing more + # left to do. indicate success to our caller. + return 1; + } - # see if path already exists, if not, create it - unless (-d $path) { - eval { mkpath($path) }; - if($@) { - return (0, sprintf($i18n->get('could not create path'), $path, $@)); - } - } + # the path exists. make sure it's actually a directory. + if(!-d $exportPath) { + WebGUI::Error->throw(error => "$exportPath isn't a directory"); + } - # output which page we're exporting - unless ($quiet) { - $self->session->output->print(sprintf($i18n->get('exporting page'), $pathWithFilename)); - } + # the path is defined, isn't an empty string, exists on disk as a directory. let's make sure we have the appropriate permissions. On Unix systems, + # we need to be able to write to the directory to create files and directories beneath it, and we need to be able to 'execute' the directory to + # list files in it. and of course we need to be able to read it too. check for all of these. + if(! (-w $exportPath) || ! (-x _) || ! (-r _) ) { + WebGUI::Error->throw(error => "can't access $exportPath"); + } - # write the file - #open my $fileHandle, ">", $pathWithFilename; - my $fileHandle = FileHandle->new(">".$pathWithFilename); - if (defined $fileHandle) { - $assetSession->output->setHandle($fileHandle); - $assetSession->asset($asset); - my $content = $asset->exportHtml_view; - # chunked content will have already been printed, so no need to print again - unless ($content eq "chunked") { - $assetSession->output->print($content); - } - $fileHandle->close; - } - else { - return (0, sprintf($i18n->get('could not open path'), $pathWithFilename, $!)); - } - $assetSession->var->end; - $assetSession->close; - $self->session->db->write("UPDATE asset SET lastExportedAs = ? WHERE assetId = ?", [$pathWithFilename, $asset->getId]); - $self->session->output->print($i18n->get('done')) unless $quiet; - $exportedCount++; - } - - # symlink? - if ($extrasUploadsAction eq 'symlink') { - my ($extrasPath, $uploadsPath) = ($self->session->config->get('extrasPath'), $self->session->config->get('uploadsPath')); - my ($extrasUrl, $uploadsUrl) = ($self->session->config->get('extrasURL'), $self->session->config->get('uploadsURL')); - s#^/*## for ($extrasUrl, $uploadsUrl); - my ($extrasDst, $uploadsDst) = ($exportPath.'/'.$extrasUrl, $exportPath.'/'.$uploadsUrl); - - $self->session->output->print($i18n->get('extrasUploads symlinking')."\n") unless $quiet; - foreach my $rec ([$extrasPath, $extrasDst], [$uploadsPath, $uploadsDst]) { - my ($path, $dst) = @$rec; - if (-l $dst) { - next if (readlink $dst eq $path); - unlink $dst or return (0, sprintf($i18n->get('could not unlink'), $dst, $!)); - } - - eval { mkpath($dst) }; - $@ and return (0, sprintf($i18n->get('could not create'), $dst, $@)); - rmdir $dst or return (0, sprintf($i18n->get('could not rmdir'), $dst, $!)); - symlink $path, $dst or return (0, sprintf($i18n->get('could not symlink'), $path, $dst, $!)); - } - } - elsif ($extrasUploadsAction eq 'none') { - # Nothing. This is the default. - } - - if ($rootUrlAction eq 'symlinkDefault') { - if (defined $defaultAssetPath) { - my ($src, $dst) = ($defaultAssetPath, $exportPath.'/'.$index); - $self->session->output->print($i18n->get('rootUrl symlinking default')."\n") unless $quiet; - if (-l $dst) { - last if (readlink $dst eq $src); - unlink $dst or return (0, sprintf($i18n->get('could not unlink'), $dst, $!)); - } - symlink $src, $dst or return (0, sprintf($i18n->get('could not symlink'), $src, $dst, $!)); - } else { - $self->session->output->print($i18n->get('rootUrl default not present')."\n") unless $quiet; - } - } elsif ($rootUrlAction eq 'none') { - # Nothing. This is the default. - } - - return (1, sprintf($i18n->get('export information'), $exportedCount, ($self->session->datetime->time()-$startTime))); + # everything checks out, return 1 + return 1; } #------------------------------------------------------------------- -=head2 _translateUrlToPath ( url, index ) +=head2 exportasHtml ( params ) -Translates a url into an appropriate path and filename for exporting +Main logic hub for export functionality. This method calls most of the rest of +the methods that handle exporting. Any exceptions thrown by the called methods +are returned as strings to the caller. Returns a status description upon +completion. Takes a hashref of arguments, containing the following keys: + +=over 4 + +=item quiet + +Boolean. To be or not to be quiet with our output. Defaults to false. + +=item userId + +The WebGUI user ID as which to perform the export. Note that this user must be +able to view the assets which you want to export (i.e., +C<$asset->canView($userId)>, or this will return a permissions error. + +If given a L object as a userId, will use that, but you didn't just read +that. + +=item indexFileName + +The file name to give to page layout and similar index files. Typically +C, and also the default. + +=item extrasUploadAction + +A string, either 'symlink' or something false, describing what to do with the +C and C directories. If 'symlink', will symlink the site's +directories into the exported content. If false, will do nothing. + +=item rootUrlAction + +The same as for C, where 'symlink' will make a symlink and +false will do nothing. + +=back 4 + +=cut + +# the general flow here works like this: +# 1. make sure the export path is valid +# 2. construct the list of assets for exporting +# 3. for each asset, check that the user can view the asset. skip it if we can't. +# 4. for each asset, check if it's exportable. skip it if it isn't. +# 5. for each asset, write its contents to disk, making all the required paths beforehand +# 6. handle symlinking if required + +sub exportAsHtml { + my $self = shift; + my $session = $self->session; + my ($returnCode, $message); + + # get the i18n object + my $i18n = WebGUI::International->new($self->session, 'Asset'); + + # take down when we started to tell the user how long the process took. + my $startTime = $session->datetime->time; + + # before even looking at the parameters, make sure the exportPath is valid. + my $exportPath = $session->config->get('exportPath'); + eval { WebGUI::Asset->exportCheckPath($session) }; + + # something went wrong. we don't really care what at this point. we did + # everything we could to try to make exporting possible and we failed. give + # up. + if($@) { + $returnCode = 0; + $message = $@; + return ($returnCode, $message); + } + + # get parameters + my $args = shift; + my $quiet = $args->{quiet}; + my $userId = $args->{userId}; + my $depth = $args->{depth}; + my $indexFileName = $args->{indexFileName}; + my $extrasUploadAction = $args->{extrasUploadAction}; + my $rootUrlAction = $args->{rootUrlAction}; + + # if we're doing symlinking of the root URL, then the current default asset + # is the root of the tree. take down that asset ID so we can check each + # asset we export against it later in the loop. that way, we can get its + # path on disk and set it as the destination of the root symlink. + my $defaultAssetId = $self->session->setting->get('defaultPage'); + my $defaultAsset = WebGUI::Asset->newByDynamicClass($session, $defaultAssetId); + + my @extraUploadActions = qw/ symlink none /; + my @rootUrlActions = qw/ symlink none /; + + # verify them + if(!defined $userId) { + $returnCode = 0; + $message = 'need a userId parameter'; + return ($returnCode, $message); + } + + # we take either a numeric userId or a WebGUI::User object + if( ref $userId ne 'WebGUI::User' && !looks_like_number($userId) ) { + $returnCode = 0; + $message = "'$userId' is not a valid userId"; + return ($returnCode, $message); + } + + # depth is required. + if(!defined $depth) { + $returnCode = 0; + $message = 'need a depth'; + return ($returnCode, $message); + } + # and it must be a number. + if( !looks_like_number($depth) ) { + $returnCode = 0; + $message = "'$depth' is not a valid depth"; + return ($returnCode, $message); + } + + # extrasUploadAction and rootUrlAction must have values matching something + # in the arrays defined above + if( defined $extrasUploadAction && !isIn($extrasUploadAction, @extraUploadActions) ) { + $returnCode = 0; + $message = "'$extrasUploadAction' is not a valid extrasUploadAction"; + return ($returnCode, $message); + } + + if( defined $rootUrlAction && !isIn($rootUrlAction, @rootUrlActions) ) { + $returnCode = 0; + $message = "'$rootUrlAction' is not a valid rootUrlAction"; + return ($returnCode, $message); + } + + # the export path is valid. the params are good. let's get started. first, + # we need to get the assets that we'll be exporting. exportGetDescendants + # takes a WebGUI::User object, so give it one. + my $user; + if(ref $userId ne 'WebGUI::User') { + $user = WebGUI::User->new($session, $userId); + } + else { + $user = $userId; + } + + my $assetIds = $self->exportGetDescendants($user, $depth); + + # now, create a new session as the user doing the exports. this is so that + # the exported assets are taken from that user's perspective. + + my $exportSession = WebGUI::Session->open($self->session->config->getWebguiRoot, $self->session->config->getFilename); + $exportSession->user( { userId => $userId } ); + + # make sure this user can view the top level asset we're exporting. if not, don't do anything. + unless ( $self->canView($userId) ) { + $returnCode = 0; + $message = "can't view asset at URL " . $self->getUrl; + return ($returnCode, $message); + } + + my $exportedCount = 0; + foreach my $assetId ( @{$assetIds} ) { + my $asset = WebGUI::Asset->newByDynamicClass($exportSession, $assetId); + my $fullPath = $asset->exportGetUrlAsPath; + + # skip this asset if we can't view it as this user. + unless( $asset->canView($userId) ) { + if( !$quiet ) { + my $message = sprintf( $i18n->get('bad user privileges') . "\n") . $asset->getUrl; + $self->session->output->print($message); + } + next; + } + + # skip this asset if it's not exportable. + unless ( $asset->exportCheckExportable ) { + if( !$quiet ) { + $self->session->output->print("$fullPath skipped, not exportable
"); + } + next; + } + + # tell the user which asset we're exporting. + unless ($quiet) { + my $message = sprintf $i18n->get('exporting page'), $fullPath; + $self->session->output->print($message); + } + + # try to write the file + eval { $asset->exportWriteFile }; + if($@) { + $returnCode = 0; + $message = $@; + $self->session->output->print("could not export asset with URL " . $asset->getUrl . ": $@"); + } + + # we exported this one successfully, so count it + $exportedCount++; + + # track when this asset was last exported for external caching and the like + $session->db->write("UPDATE asset SET lastExportedAs = ? WHERE assetId = ?", [$fullPath, $asset->getId]); + + # tell the user we did this asset correctly + $session->output->print($i18n->get('done')) unless $quiet; + } + + # handle symlinking + if($extrasUploadAction eq 'symlink') { + eval { WebGUI::Asset->exportSymlinkExtrasUploads($session) }; + if ($@) { + $returnCode = 0; + $message = $@; + return ($returnCode, $message); + } + } + + if($rootUrlAction eq 'symlink') { + eval { WebGUI::Asset->exportSymlinkRoot($session, $defaultAsset, $indexFileName, $quiet) }; + if ($@) { + $returnCode = 0; + $message = $@; + return ($returnCode, $message); + } + } + + # we don't need the session any more, so close it. + $exportSession->var->end; + $exportSession->close; + + # we're done. give the user a status report. + $returnCode = 1; + my $timeRequired = $session->datetime->time - $startTime; + $message = sprintf $i18n->get('export information'), $exportedCount, $timeRequired; + return ($returnCode, $message); +} + +#------------------------------------------------------------------- + +=head2 exportCheckExportable ( ) + +Determines whether this asset is exportable, first by checking whether all of +its parents are exportable and then by checking the asset itself. Returns a +boolean indicating whether or not this asset is exportable. + +=cut + +sub exportCheckExportable { + my $self = shift; + + # get this asset's ancestors. return objects as a shortcut since we'd be instantiating them all anyway. + my $assets = $self->getLineage( ['ancestors'], { returnObjects => 1 } ); + + # process each one. return false if any of the assets in the lineage, or this asset itself, isn't exportable. + foreach my $asset ( @{$assets}, $self ) { + return 0 unless $asset->get('isExportable'); + } + + # passed checks, return 1 + return 1; +} + +#------------------------------------------------------------------- + +=head2 exportGetDescendants ( userId, depth ) + +Gets the descendants of this asset for exporting, walking the lineage as the +userId specified. Throws an exception if the given user ID is invalid. Takes the +following parameters: + +=over 4 + +=item userId + +The WebGUI user ID as which to do the export. If not specified, performs the +export as the current session's user. + +=item depth + +The depth to pass to getLineage. How many levels in the lineage to go. + +=back 4 + +Throws the following exceptions: + +=over 4 + +=item WebGUI::Error::InvalidObject + +The given WebGUI user ID is not valid. + +=item WebGUI::Error::InvalidParam + +The value given for depth is invalid. + +=back + +=cut + +sub exportGetDescendants { + my $self = shift; + my $user = shift; + my $depth = shift; + + # check for parameter validity + if( (!defined $user) or (ref $user ne 'WebGUI::User') ) { + WebGUI::Error::InvalidObject->throw(expected => 'WebGUI::User', got => ref $user, error => 'Need a WebGUI::User object', param => $user); + } + + if( (!defined $depth) or (!looks_like_number($depth)) ) { + WebGUI::Error::InvalidParam->throw(error => 'Need a depth', param => $depth); + } + + # open a temporary session as the user doing the exporting so we don't get assets that they can't see + my $tempSession = WebGUI::Session->open($self->session->config->getWebguiRoot, $self->session->config->getFilename); + $tempSession->user( { userId => $user->userId } ); + + # clone self in the new session and get its lineage as the new user + my $cloneOfSelf = WebGUI::Asset->new($tempSession, $self->getId, $self->get('className'), $self->get('revisionDate')); + my $assetIds = $cloneOfSelf->getLineage( [ "self", "descendants" ], { + endingLineageLength => $cloneOfSelf->getLineageLength + $depth, + orderByClause => 'assetData.url DESC', + } ); + + # properly close the temp session + $tempSession->var->end; + $tempSession->close; + + return $assetIds; +} + +#------------------------------------------------------------------- + +=head2 exportGetUrlAsPath ( index ) + +Translates a URL into an appropriate path and filename for exporting. For +example, given C<'/foo/bar/baz'>, will return C<'/foo/bar/baz/index.html'> +provided the value of indexFile as given to exportAsHtml was C<'index.html'>. =head3 url @@ -244,58 +469,273 @@ URL of the asset we need an export path for =head3 index -index filename passed in from the UI +index filename passed from L =cut -sub _translateUrlToPath { - my $self = shift; - my $url = shift; - my $index = shift; - my $dataRef; +sub exportGetUrlAsPath { + my $self = shift; + my $index = shift || 'index.html'; - # Ignore trailing slashes - $url =~ s{/+$}{}g; + my $config = $self->session->config; + my $exportPath = $config->get('exportPath'); + + # specify a list of file types apache recognises to be passed through as-is + my @fileTypes = qw/.html .htm .txt .pdf .jpg .css .gif .png .doc .xls .xml .rss .bmp .mp3 .js .fla .flv .swf .pl .php .php3 .php4 .php5 .ppt .docx .zip .tar .rar .gz .bz2/; - # If there is not a dot in the URL, this is easy - if ($url !~ m{[.]}) { - $dataRef->{'path' } = $url; - $dataRef->{'filename' } = $index; - } - # There is a dot - else { - # The last part after a slash is the "name" - my ($path,$name) = $url =~ m{(?:(.*) /)? ([^/]+) $}x; # NOTE: Might be more efficient to use index() and substr() + # get the asset's URL as a URI::URL object for easy parsing of components + my $url = URI::URL->new($config->get("sitename")->[0] . $self->getUrl); + my @pathComponents = $url->path_components; + shift @pathComponents; # first item is the empty string + my $filename = pop @pathComponents; - # If it ends in a known file type handled by apache, use that - if ($name =~ m{[.](?:html|htm|txt|pdf|jpg|css|gif|png|doc|xls|xml|rss|bmp|mp3|js|fla|flv|swf)$}) { - $dataRef->{'path' } = $path; - $dataRef->{'filename' } = $name; + # if there's no . (i.e., no file with an extension) in $filename, this is + # simple. Slap on a directory separator, $index, and return it. + if(!index $filename, '.') { # no need to regex match for a single character + return Path::Class::File->new($exportPath, @pathComponents, $filename, $index); + } + else { # got a dot + my $extension = (fileparse($filename, qr/\.[^.]*/))[2]; # get just the extension + + # check if the file type is recognised by apache. if it is, return it + # as-is. if not, slap on the directory separator, $index, and return + # it. + if( isIn($extension, @fileTypes) ) { + return Path::Class::File->new($exportPath, @pathComponents, $filename); } - else { - # It doesn't end in a known file type - # Make a directory for it - $dataRef->{'path' } = $url; - $dataRef->{'filename' } = $index; + else { # don't know what it is + return Path::Class::File->new($exportPath, @pathComponents, $filename, $index); } } - - return $dataRef; } #------------------------------------------------------------------- -=head2 exportAsHtml +=head2 exportSymlinkExtrasUploads -Same as www_exportGenerate except without the output. Returns -"success" if successful, otherwise returns an error message. +Class method. Sets up the extras and uploads symlinks. + +Takes the following parameters: + +=over 4 + +=item session + +A reference to a L object. + +Throws the following exceptions: + +=over 4 + +=item WebGUI::Error + +Encountered a filesystem error in setting up the links. + +=item WebGUI::InvalidObject + +The first parameter is not a L. + +=back 4 =cut -sub exportAsHtml { - my $self = shift; - my ($success, $description) = $self->_exportAsHtml(1, '1', 'index.html'); - return $success? "success" : $description; +sub exportSymlinkExtrasUploads { + my $class = shift; + my $session = shift; + + # check that session is a valid WebGUI::Session object + if(!defined $session || ref $session ne 'WebGUI::Session') { + WebGUI::Error::InvalidObject->throw(error => "first param to exportSymlinkExtrasUploads must be a WebGUI::Session"); + } + + my $config = $session->config; + my $extrasPath = $config->get('extrasPath'); + my $extrasUrl = $config->get('extrasURL'); + my $uploadsPath = $config->get('uploadsPath'); + my $uploadsUrl = $config->get('uploadsURL'); + + # we have no assurance whether the exportPath is valid or not, so check it. + WebGUI::Asset->exportCheckPath($session); + + # if we're still here, it's valid + my $exportPath = $config->get('exportPath'); + + # chop off leading / so we don't accidentally get absolute paths + s#^/*## for ($extrasUrl, $uploadsUrl); + + # construct the destination paths + my $extrasDst = Path::Class::File->new($exportPath, $extrasUrl)->absolute->stringify; + my $uploadsDst = Path::Class::File->new($exportPath, $uploadsUrl)->absolute->stringify; + + # for each of extras and uploads, do the following: + # check of the destination path exists and is a symlink + # if it is, assume it's from a prior exporting and remove it + # if that doesn't work, throw an exception + # if that does work, symlink the on-disk path to the destination + # if that doesn't work, throw an exception + + foreach my $rec ([$extrasPath, $extrasDst], [$uploadsPath, $uploadsDst]) { + my ($path, $dst) = @$rec; + if (-l $dst) { + next if (readlink $dst eq $path); + unlink $dst or WebGUI::Error->throw(error => "could not unlink $dst: $!"); + } + + # the path holding the symlinks is the export path, which exists at + # this point + symlink $path, $dst or WebGUI::Error->throw(error => "could not symlink $path, $dst: $!"); + } +} + +#------------------------------------------------------------------- + +=head2 exportSymlinkRoot ( session, defaultAsset, [indexFile], [quiet] ) + +Class method. Places a symlink in the exportPath linking to the index file of +the default asset. + +Takes the following parameters: + +=over 4 + +=item session + +A reference to a L object. + +Throws the following exceptions: + +=item defaultAsset + +The path to this asset's exported location on disk will be the target of the +symlink for the root URL. + +=item indexFile + +Optional. Specifies a file name for the index URL. Defaults to C. + +=item quiet + +Optional. Whether to be quiet with our output. + +=over 4 + +=item WebGUI::Error + +Encountered a filesystem error in setting up the link. + +=item WebGUI::InvalidObject + +The first parameter is not a L. + +=back 4 + +=cut + +sub exportSymlinkRoot { + my $class = shift; + + my $session = shift; + my $defaultAsset = shift; + my $index = shift || 'index.html'; + my $quiet = shift; + + # check that $session is valid + if(!defined $session || ref $session ne 'WebGUI::Session') { + WebGUI::Error::InvalidObject->throw(error => 'first param to exportSymlinkRoot must be a WebGUI::Session'); + } + + # check that $defaultAsset is valid + if( !defined $defaultAsset || !$defaultAsset->isa('WebGUI::Asset') ) { + WebGUI::Error::InvalidParam->throw(error => 'second param to exportSymlinkRoot must be the default asset', param => $defaultAsset); + } + + # can't be sure if the export path exists, so check it. + WebGUI::Asset->exportCheckPath($session); + + # if we're still here, it's valid, so get it + my $exportPath = $session->config->get('exportPath'); + + # get the source and the destination + my $source = $defaultAsset->exportGetUrlAsPath->absolute->stringify; + my $destination = Path::Class::File->new($exportPath, $index)->absolute->stringify; + + my $i18n = WebGUI::International->new($session, 'Asset'); + + # tell the user what's happening + if( !$quiet ) { + my $message = $i18n->get('rootUrl symlinking default') . "\n"; + $session->output->print($message); + } + + # if the link exists, check if it's set up properly. if it's not, remove it. + if (-l $destination) { + return if readlink $destination eq $source; + unlink $destination or WebGUI::Error->throw(error => sprintf($i18n->get('could not unlink'), $destination, $!)); + } + symlink $source, $destination or WebGUI::Error->throw(error => sprintf($i18n->get('could not symlink'), $source, $destination, $!)); +} + +#------------------------------------------------------------------- + +=head2 exportWriteFile ( ) + +Creates required directories, gathers the content for this particular exported +file, and writes that content to disk. + +Throws the following exceptions: + +=over 4 + +=item WebGUI::Error + +Insufficient privileges for writing to the FS path as this OS user, or +insufficient viewing privileges for the asset. + +=back + +=cut + +sub exportWriteFile { + my $self = shift; + + # we have no assurance whether the exportPath is valid or not, so check it. + WebGUI::Asset->exportCheckPath($self->session); + + # if we're still here, everything is well with the export path. let's make + # sure that this user can view the asset that we want to export. + unless($self->canView) { + WebGUI::Error->throw(error => "user can't view asset at " . $self->getUrl . " to export it"); + } + + + # if we're still here, everything is well with the export path. let's get + # our destination FS path and then make any required directories. + + my $dest = $self->exportGetUrlAsPath; + my $parent = $dest->parent; + + eval { mkpath($parent->absolute->stringify) }; + if($@) { + WebGUI::Error->throw(error => "could not make directory " . $parent->absolute->stringify); + } + + # next, get the contents, open the file, and write the contents to the file. + my $fh = eval { $dest->openw }; + if($@) { + WebGUI::Error->throw(error => "can't open " . $dest->absolute->stringify . " for writing: $!"); + } + my $previousHandle = $self->session->{_handle}; + my $previousDefaultAsset = $self->session->asset; + $self->session->asset($self); + $self->session->output->setHandle($fh); + my $contents = $self->exportHtml_view; + + # chunked content is already printed, no need to print it again + unless($contents eq 'chunked') { + $self->session->output->print($contents); + } + + $self->session->output->setHandle($previousHandle); } #------------------------------------------------------------------- @@ -322,53 +762,60 @@ Displays the export page administrative interface =cut sub www_export { - my $self = shift; + my $self = shift; return $self->session->privilege->insufficient() unless ($self->session->user->isInGroup(13)); - my $i18n = WebGUI::International->new($self->session, "Asset"); - my $f = WebGUI::HTMLForm->new($self->session,-action=>$self->getUrl); - $f->hidden( - -name => "func", - -value => "exportStatus" - ); - $f->integer( - -label=>$i18n->get('Depth'), - -hoverHelp=>$i18n->get('Depth description'), - -name=>"depth", - -value=>99, - ); - $f->selectBox( - -label=>$i18n->get('Export as user'), - -hoverHelp=>$i18n->get('Export as user description'), - -name=>"userId", - -options=>$self->session->db->buildHashRef("select userId, username from users"), - -value=>[1], - ); - $f->text( - -label=>$i18n->get("directory index"), - -hoverHelp=>$i18n->get("directory index description"), - -name=>"index", - -value=>"index.html" - ); + my $i18n = WebGUI::International->new($self->session, "Asset"); + my $f = WebGUI::HTMLForm->new($self->session, -action => $self->getUrl); + $f->hidden( + -name => "func", + -value => "exportStatus" + ); + $f->integer( + -label => $i18n->get('Depth'), + -hoverHelp => $i18n->get('Depth description'), + -name => "depth", + -value => 99, + ); + $f->selectBox( + -label => $i18n->get('Export as user'), + -hoverHelp => $i18n->get('Export as user description'), + -name => "userId", + -options => $self->session->db->buildHashRef("select userId, username from users"), + -value => [1], + ); + $f->text( + -label => $i18n->get("directory index"), + -hoverHelp => $i18n->get("directory index description"), + -name => "index", + -value => "index.html" + ); - # TODO: maybe add copy options to these boxes alongside symlink - $f->selectBox( - -label => $i18n->get('extrasUploads form label'), - -hoverHelp => $i18n->get('extrasUploads form hoverHelp'), - -name => "extrasUploadsAction", - -options => { 'symlink' => $i18n->get('extrasUploads form option symlink'), - 'none' => $i18n->get('extrasUploads form option none') }, - -value => ['none'], - ); - $f->selectBox( - -label => $i18n->get('rootUrl form label'), - -hoverHelp => $i18n->get('rootUrl form hoverHelp'), - -name => "rootUrlAction", - -options => { 'symlinkDefault' => $i18n->get('rootUrl form option symlinkDefault'), - 'none' => $i18n->get('rootUrl form option none') }, - -value => ['none'], - ); - $f->submit; - $self->getAdminConsole->render($self->checkExportPath.$f->print,$i18n->get('Export Page')); + # TODO: maybe add copy options to these boxes alongside symlink + $f->selectBox( + -label => $i18n->get('extrasUploads form label'), + -hoverHelp => $i18n->get('extrasUploads form hoverHelp'), + -name => "extrasUploadsAction", + -options => { + 'symlink' => $i18n->get('extrasUploads form option symlink'), + 'none' => $i18n->get('extrasUploads form option none') }, + -value => ['none'], + ); + $f->selectBox( + -label => $i18n->get('rootUrl form label'), + -hoverHelp => $i18n->get('rootUrl form hoverHelp'), + -name => "rootUrlAction", + -options => { + 'symlink' => $i18n->get('rootUrl form option symlinkDefault'), + 'none' => $i18n->get('rootUrl form option none') }, + -value => ['none'], + ); + $f->submit; + my $message; + eval { WebGUI::Asset->exportCheckPath($self->session) }; + if($@) { + $message = $@; + } + $self->getAdminConsole->render($message . $f->print, $i18n->get('Export Page')); } @@ -381,16 +828,16 @@ Displays the export status page =cut sub www_exportStatus { - my $self = shift; + my $self = shift; return $self->session->privilege->insufficient() unless ($self->session->user->isInGroup(13)); - my $i18n = WebGUI::International->new($self->session, "Asset"); - my $iframeUrl = $self->getUrl('func=exportGenerate'); + my $i18n = WebGUI::International->new($self->session, "Asset"); + my $iframeUrl = $self->getUrl('func=exportGenerate'); foreach my $formVar (qw/index depth userId extrasUploadsAction rootUrlAction/) { - $iframeUrl = $self->session->url->append($iframeUrl, $formVar.'='.$self->session->form->process($formVar)); + $iframeUrl = $self->session->url->append($iframeUrl, $formVar . '=' . $self->session->form->process($formVar)); } - my $output = ''; - $self->getAdminConsole->render($output,$i18n->get('Page Export Status'),"Asset"); + my $output = ''; + $self->getAdminConsole->render($output, $i18n->get('Page Export Status'), "Asset"); } #------------------------------------------------------------------- @@ -402,27 +849,30 @@ Executes the export process and displays real time status. This operation is dis =cut sub www_exportGenerate { - my $self = shift; - return $self->session->privilege->insufficient() unless ($self->session->user->isInGroup(13)); + my $self = shift; + return $self->session->privilege->insufficient() unless ($self->session->user->isInGroup(13)); # This routine is called in an IFRAME and prints status output directly to the browser. - $|++; # Unbuffered data output - $self->session->style->useEmptyStyle(1); - $self->session->http->sendHeader; + $|++; # Unbuffered data output + $self->session->style->useEmptyStyle(1); + $self->session->http->sendHeader; - my $i18n = WebGUI::International->new($self->session, 'Asset'); - my ($success, $description) = - $self->_exportAsHtml(0, $self->session->form->process('userId'), - $self->session->form->process('index'), - $self->session->form->process('extrasUploadsAction'), - $self->session->form->process('rootUrlAction')); - if (!$success) { - $self->session->output->print($description,1); - return "chunked"; - } + my $i18n = WebGUI::International->new($self->session, 'Asset'); + my ($success, $description) = $self->exportAsHtml( { + quiet => 0, + userId => $self->session->form->process('userId'), + indexFileName => $self->session->form->process('index'), + extrasUploadAction => $self->session->form->process('extrasUploadsAction'), + rootUrlAction => $self->session->form->process('rootUrlAction'), + depth => $self->session->form->process('depth'), + }); + if (!$success) { + $self->session->output->print($description, 1); + return "chunked"; + } - $self->session->output->print($description,1); - $self->session->output->print(''.$i18n->get(493,'WebGUI').''); - return "chunked"; + $self->session->output->print($description, 1); + $self->session->output->print('' . $i18n->get(493, 'WebGUI') . ''); + return "chunked"; } 1; diff --git a/lib/WebGUI/Workflow/Activity/ExportVersionTagToHtml.pm b/lib/WebGUI/Workflow/Activity/ExportVersionTagToHtml.pm index 49649456b..b9cde72ff 100644 --- a/lib/WebGUI/Workflow/Activity/ExportVersionTagToHtml.pm +++ b/lib/WebGUI/Workflow/Activity/ExportVersionTagToHtml.pm @@ -71,9 +71,9 @@ sub execute { my $self = shift; my $versionTag = shift; foreach my $asset (@{$versionTag->getAssets}) { - my $status = $asset->exportAsHtml(); + my ($returnCode, $status) = $asset->exportAsHtml( { quiet => 1, userId => 1, indexFileName => 'index.html', depth => 99, } ); return $self->ERROR unless ($status eq "success"); - $status = $asset->getContainer->exportAsHtml(); + ($returnCode, $status) = $asset->getContainer->exportAsHtml( { quiet => 1, userId => 1, indexFileName => 'index.html', depth => 99, } ); return $self->ERROR unless ($status eq "success"); } return $self->COMPLETE; diff --git a/sbin/testEnvironment.pl b/sbin/testEnvironment.pl index ec36ac2c8..c17a34d8f 100644 --- a/sbin/testEnvironment.pl +++ b/sbin/testEnvironment.pl @@ -124,6 +124,7 @@ checkModule("Class::InsideOut","1.06"); checkModule("HTML::TagCloud","0.34"); checkModule("Image::ExifTool","7.00"); checkModule("Archive::Any","0.093"); +checkModule("Path::Class", '0.16'); ################################### diff --git a/t/Asset/AssetExportHtml.t b/t/Asset/AssetExportHtml.t index 00a7a2a8f..5291cbffb 100644 --- a/t/Asset/AssetExportHtml.t +++ b/t/Asset/AssetExportHtml.t @@ -1,80 +1,991 @@ +# vim:syntax=perl #------------------------------------------------------------------- # WebGUI is Copyright 2001-2008 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 -#------------------------------------------------------------------- +#------------------------------------------------------------------ + +# These tests are for the shiny rewritten export functionality. it tries +# really hard to test every permutation of the code. use FindBin; use strict; use lib "$FindBin::Bin/../lib"; -use WebGUI::Test; -use WebGUI::Session; - -# load your modules here - use Test::More; +use WebGUI::Test; # Must use this before any other WebGUI modules +use WebGUI::PseudoRequest; -my $session = WebGUI::Test->session; +use WebGUI::Session; +use WebGUI::Asset; +use WebGUI::Exception; -## Test the url to path/file translation -my $index = "home.html"; -my $urlMap = { - 'index.html' => { - path => undef, - filename => 'index.html', - }, +use Cwd; +use Exception::Class; +use File::Path; +use File::Temp qw/tempfile tempdir/; +use File::Slurp; +use Path::Class; +use Test::Deep; - - 'index.html/' => { - path => undef, - filename => 'index.html', - }, +#---------------------------------------------------------------------------- +# Init +my $session = WebGUI::Test->session; +$session->{_request} = WebGUI::PseudoRequest->new(); - 'foo/page.html' => { - path => 'foo', - filename => 'page.html', - }, - - 'foo' => { - path => 'foo', - filename => $index, - }, +#---------------------------------------------------------------------------- +# Tests - 'index.html/foo' => { - path => 'index.html/foo', - filename => $index, - }, +plan tests => 145; # Increment this number for each test you create - 'index.html/foo.html' => { - path => 'index.html', - filename => 'foo.html', - }, +#---------------------------------------------------------------------------- +# exportCheckPath() + +my $e; + +# ensure exportCheckPath barfs if not given a session as its first argument. +eval { WebGUI::Asset->exportCheckPath() }; +$e = Exception::Class->caught(); +isa_ok($e, 'WebGUI::Error::InvalidObject', 'exportCheckPath tests that its argument is a WebGUI::Session'); +cmp_deeply( + $e, + methods( + error => "first param to exportCheckPath must be a WebGUI::Session", + ), + "exportCheckPath tests that its argument is a WebGUI::Session" +); + +# need to test that exportCheckPath() barfs on an undefined exportPath. To do +# this, we need to make sure that exportPath is undefined. However, completely +# wiping out someone's exportPath setting isn't precisely the paragon of +# politeness. Take a backup of the current exportPath before undefining it. + +my $originalExportPath = $session->config->get('exportPath'); +my $config = $session->config; +$config->delete('exportPath'); + +eval { WebGUI::Asset->exportCheckPath($session) }; +$e = Exception::Class->caught(); +isa_ok($e, 'WebGUI::Error', "exportCheckPath throws if exportPath isn't defined"); +cmp_deeply( + $e, + methods( + error => 'exportPath must be defined and not ""', + ), + "exportCheckPath throws if exportPath isn't defined" +); + +# we'll restore the original exportPath setting after performing these tests. +# for now, we need a controlled environment. + +# first, let's test a directory to which we hopefully cannot write. +my $rootDirectory = Path::Class::Dir->new(''); +$config->set('exportPath', $rootDirectory->stringify); + +eval { WebGUI::Asset->exportCheckPath($session) }; +$e = Exception::Class->caught(); +isa_ok($e, 'WebGUI::Error', "exportCheckPath throws if we can't access the exportPath"); +cmp_deeply( + $e, + methods( + error => "can't access $rootDirectory", + ), + "exportCheckPath throws if we can't access the exportPath" +); + +# next, let's set the exportPath to a non-directory file and make sure that it explodes. +my $exportPathFile; +(undef, $exportPathFile) = tempfile('webguiXXXXX', UNLINK => 1); +$config->set('exportPath', $exportPathFile); + +eval { WebGUI::Asset->exportCheckPath($session) }; +$e = Exception::Class->caught(); +isa_ok($e, 'WebGUI::Error', "exportCheckPath throws if exportPath is a file rather than a directory"); +cmp_deeply( + $e, + methods( + error => "$exportPathFile isn't a directory", + ), + "exportCheckPath throws if exportPath is a file rather than a directory" +); + +# next, let's find a directory to which we can write, but change it so that we +# *can't* write to it. exportCheckPath will try to create the exportPath if it's +# a subdirectory of a path that exists, so let's make sure this exception works. + +my $tempDirectory = tempdir('webguiXXXXX', CLEANUP => 1); +my $inaccessibleDirectory = Path::Class::Dir->new($tempDirectory, 'unwritable'); +chmod 0000, $tempDirectory; +$config->set('exportPath', $inaccessibleDirectory->stringify); + +eval { WebGUI::Asset->exportCheckPath($session) }; +$e = Exception::Class->caught(); +isa_ok($e, 'WebGUI::Error', "exportCheckPath throws if it can't create the directory it needs"); +cmp_deeply( + $e, + methods( + error => "can't create exportPath $inaccessibleDirectory", + ), + "exportCheckPath throws if it can't create the directory it needs" +); + +# we're finished making sure that the code explodes on bad stuff, so let's make +# sure that it really works when it's really supposed to. +my $returnCode; + +# first, let's try the simplest route: a directory that we know exists, that we +# know we can write to. we already have a directory we know we can write to +# (created above as a temporary directory), so let's change its permissions back +# to something sane and then test to make sure it works. + +chmod 0755, $tempDirectory; # $inaccessibleDirectory is now accessible +my $accessibleDirectory = $inaccessibleDirectory; +$config->set('exportPath', $tempDirectory); + + +eval { $returnCode = WebGUI::Asset->exportCheckPath($session) }; +is($@, '', "exportCheckPath with valid path lives"); +is($returnCode, 1, "exportCheckPath returns true value"); + +# now, let's try a directory to which we know we have access, but a path within +# it that doesn't exist. + +$config->set('exportPath', $accessibleDirectory->stringify); # now accessible! + +eval { $returnCode = WebGUI::Asset->exportCheckPath($session) }; +is($@, '', "exportCheckPath creating subdirectory lives"); +is($returnCode, 1, "exportCheckPath creating subdirectory returns true value"); +is(-d $accessibleDirectory, 1, "exportCheckPath creating subdirectory actually creates said subdirectory"); + +#---------------------------------------------------------------------------- +# exportCheckExportable() + +my $isExportable; +# simple test first. the asset we're checking isn't exportable. should of course return 0. +my $home = WebGUI::Asset->newByUrl($session, '/home'); +$home->update({ isExportable => 0 }); +$isExportable = $home->exportCheckExportable; +is($isExportable, 0, "exportCheckExportable simple check without lineage for non-exportable asset returns 0"); + +# next, make the parent exportable, but the child not exportable. test that this returns 0 as well. +$home->update({ isExportable => 1 }); +my $gettingStarted = WebGUI::Asset->newByUrl($session, '/getting_started'); +$gettingStarted->update({ isExportable => 0 }); +$isExportable = $gettingStarted->exportCheckExportable; +is($isExportable, 0, "exportCheckExportable nonexportable asset, exportable parent returns 0"); + +# next, make both non-exportable. test that this returns 0. +$home->update({ isExportable => 0 }); +$isExportable = $gettingStarted->exportCheckExportable; +is($isExportable, 0, "exportCheckExportable nonexportable asset, nonexportable parent returns 0"); + +# go another level deeper. asset, parent, grandparent. +my $grandChild = WebGUI::Asset->newByUrl($session, '/getting_started/getting-started'); + +# make it not exportable, but both parents are. still returning 0. +$grandChild->update({ isExportable => 0 }); +$home->update({ isExportable => 1 }); +$gettingStarted->update({ isExportable => 1 }); +$isExportable = $grandChild->exportCheckExportable; +is($isExportable, 0, "exportCheckExportable nonexportable asset, exportable parent and grandparent returns 0"); + +# make parent not exportable. still returning 0. +$gettingStarted->update({ isExportable => 0 }); +$isExportable = $grandChild->exportCheckExportable; +is($isExportable, 0, "exportCheckExportable nonexportable asset, parent, exportable grandparent returns 0"); + +# switch: exportable parent, nonexportable grandparent. still 0. +$gettingStarted->update({ isExportable => 1 }); +$home->update({ isExportable => 0 }); +$isExportable = $grandChild->exportCheckExportable; +is($isExportable, 0, "exportCheckExportable nonexportable asset, grandparent, exportable parent returns 0"); + +# none of asset, parent, grandparent are exportable. still 0. +$home->update({ isExportable => 0 }); +$gettingStarted->update({ isExportable => 0 }); +$isExportable = $grandChild->exportCheckExportable; +is($isExportable, 0, "exportCheckExportable nonexportable asset, grandparent, parent returns 0"); + +# finally, make everything exportable. make sure each one returns 1. +$home->update({ isExportable => 1 }); +$gettingStarted->update({ isExportable => 1 }); +$grandChild->update({ isExportable => 1 }); + +$isExportable = $home->exportCheckExportable; +is($isExportable, 1, "exportCheckExportable simple check without lineage for exportable asset returns 1"); + +$isExportable = $gettingStarted->exportCheckExportable; +is($isExportable, 1, "exportCheckExportable exportable asset, parent returns 1"); + +$isExportable = $grandChild->exportCheckExportable; +is($isExportable, 1, "exportCheckExportable exportable asset, parent, grandparent returns 1"); + +#---------------------------------------------------------------------------- +# exportGetUrlAsPath() + +# exportPath won't be changing any more, so store it. +$config->set('exportPath', $originalExportPath); +my $exportPath = $originalExportPath; + +my $litmus; +# start with something simple: export the root URL. +my $homeAsPath = $home->exportGetUrlAsPath('index.html'); +$litmus = Path::Class::File->new($exportPath, $home->getUrl, 'index.html'); +isa_ok($homeAsPath, 'Path::Class::File', 'exportGetUrlAsPath returns a Path::Class::File object'); +is($homeAsPath->absolute($exportPath)->stringify, $litmus->absolute($exportPath)->stringify, "exportGetUrlAsPath works for root directory"); + +# make sure that 'index.html' is the default file name if none given. +$homeAsPath = $home->exportGetUrlAsPath(); +$litmus = Path::Class::File->new($exportPath, $home->getUrl, 'index.html'); +isa_ok($homeAsPath, 'Path::Class::File', 'exportGetUrlAsPath without index file returns a Path::Class::File object'); +is($homeAsPath->absolute($exportPath)->stringify, $litmus->absolute($exportPath)->stringify, "exportGetUrlAsPath without index file works for root directory"); + +# let's go down a level. add a directory. +my $gsAsPath = $gettingStarted->exportGetUrlAsPath('index.html'); +$litmus = Path::Class::File->new($exportPath, $gettingStarted->getUrl, 'index.html'); +isa_ok($gsAsPath, 'Path::Class::File', 'exportGetUrlAsPath for getting_started returns a Path::Class::File object'); +is($gsAsPath->absolute($exportPath)->stringify, $litmus->absolute($exportPath)->stringify, "exportGetUrlAsPath for getting_started works for root directory"); + +# ensure 'index.html' works for a single directory. +$gsAsPath = $gettingStarted->exportGetUrlAsPath(); +isa_ok($gsAsPath, 'Path::Class::File', 'exportGetUrlAsPath for getting_started without index file returns a Path::Class::File object'); +is($gsAsPath->absolute($exportPath)->stringify, $litmus->absolute($exportPath)->stringify, "exportGetUrlAsPath for getting_started without index file works for root directory"); + +# down another level. +my $gcAsPath = $grandChild->exportGetUrlAsPath('index.html'); +$litmus = Path::Class::File->new($exportPath, $grandChild->getUrl, 'index.html'); +isa_ok($gcAsPath, 'Path::Class::File', 'exportGetUrlAsPath for grandchild returns a Path::Class::File object'); +is($gcAsPath->absolute($exportPath)->stringify, $litmus->absolute($exportPath)->stringify, "exportGetUrlAsPath for grandchild works for root directory"); + +# without index.html +my $gcAsPath = $grandChild->exportGetUrlAsPath(); +$litmus = Path::Class::File->new($exportPath, $grandChild->getUrl, 'index.html'); +isa_ok($gcAsPath, 'Path::Class::File', 'exportGetUrlAsPath for grandchild without index file returns a Path::Class::File object'); +is($gcAsPath->absolute($exportPath)->stringify, $litmus->absolute($exportPath)->stringify, "exportGetUrlAsPath for grandchild without index file works for root directory"); + +# now let's get tricky and test different file extensions +my $storage = WebGUI::Storage->create($session); +my $filename = 'somePerlFile_pl.txt'; +$storage->addFileFromScalar($filename, $filename); +$session->user({userId=>3}); +my $versionTag = WebGUI::VersionTag->getWorking($session); +$versionTag->set({name=>"Asset Export Test"}); +my $properties = { + # '1234567890123456789012' + id => 'ExportTest000000000001', + title => 'Export Test', + className => 'WebGUI::Asset::File', + url => 'export-test.pl', }; +my $defaultAsset = WebGUI::Asset->getDefault($session); +my $asset = $defaultAsset->addChild($properties, $properties->{id}); +$asset->update({ + storageId => $storage->getId, + filename => $filename, +}); -# two tests for each key in the urlMap hashRef plus any other tests that are added later. -plan tests => (0 + scalar(keys %{$urlMap}) * 2); +my $fileAsPath = $asset->exportGetUrlAsPath('index.html'); -foreach my $urlToTest ( keys %{$urlMap} ) { - my $expectedPath = $urlMap->{$urlToTest}->{'path'}; - my $expectedFilename = $urlMap->{$urlToTest}->{'filename'}; +# .pl files are recognised by apache, so are passed through as-is +$litmus = Path::Class::File->new($exportPath, $asset->getUrl); +isa_ok($fileAsPath, 'Path::Class::File', 'exportGetUrlAsPath for perl file returns a Path::Class::File object'); +is($fileAsPath->absolute($exportPath)->stringify, $litmus->absolute($exportPath)->stringify, 'exportGetUrlAsPath for perl file works'); - # we need a dummy asset to test this private method. - my $asset = WebGUI::Asset->newByPropertyHashRef($session,{className=>'WebGUI::Asset'}); +# test a different extension, the .foobar extension +$storage = WebGUI::Storage->create($session); +$filename = 'someFoobarFile.foobar'; +$storage->addFileFromScalar($filename, $filename); +$properties = { + id => 'ExportTest000000000002', + title => 'Export Test', + className => 'WebGUI::Asset::File', + url => 'export-test.foobar', +}; +$asset = $defaultAsset->addChild($properties, $properties->{id}); +$asset->update({ + storageId => $storage->getId, + filename => $filename, +}); - # test this url - my $dataRef = $asset->_translateUrlToPath($urlToTest, $index); - my $returnedPath = $dataRef->{'path'}; - my $returnedFilename = $dataRef->{'filename'}; +$fileAsPath = $asset->exportGetUrlAsPath('index.html'); +# not recognised by apache, so it'll add an index.html, make sure it does so +$litmus = Path::Class::File->new($exportPath, $asset->getUrl, 'index.html'); +isa_ok($fileAsPath, 'Path::Class::File', 'exportGetUrlAsPath for plain file returns a Path::Class::File object'); +is($fileAsPath->absolute($exportPath)->stringify, $litmus->absolute($exportPath)->stringify, 'exportGetUrlAsPath for plain file works'); - is ($returnedPath, $expectedPath, "path $expectedPath was returned for url $urlToTest"); - is ($returnedFilename, $expectedFilename, "filename $expectedFilename was returned for url $urlToTest"); +#---------------------------------------------------------------------------- +# exportWriteFile() + +# we'll be writing real on-disk files and directories for these tests. do our +# level best at cleaning up after ourselves. this is taken care of in the END +# block via rmtree(). +# ideally, exportCheckPath will have been called before exportWriteFile(), but +# we can't be certain of that. this means that we may not have permission to +# write to the exportPath, or the exportPath may not even exist. there's also a +# race condition that exists between the time exportCheckPath() ran and the +# time exportWriteFile() attempts to write files to disk. it's pathological, +# yes, but I'm really not interested in tracking down the kinds of bugs that +# these race conditions can create. so exportWriteFile() will check for the +# actual ability to make all of the paths it requires and for the ability to +# write the files it needs. +# so, let's get started with a bad export path. set it to something that +# shouldn't exist first. this should try to create it. rather than testing two +# parts of the code (the nonexistent directory check and the creation success +# check) at once, let's make it something that we *can* create. probably the +# best way to generate something that we can guarantee doesn't exist is to use +# a GUID. + +# we need to be tricky here and call code in wG proper which calls www_ methods +# even though we don't have access to modperl. the following hack lets us do +# that. +$session->http->{_http}->{noHeader} = 1; + +$session->user( { userId => 1 } ); +my $content; +my $guid = $session->id->generate; +my $guidPath = Path::Class::Dir->new($config->getWebguiRoot, '..', 'domains', $guid); +$config->set('exportPath', $guidPath->absolute->stringify); +eval { $home->exportWriteFile() }; +is($@, '', "exportWriteFile works when creating exportPath"); + +# ensure that the file was actually written +ok(-e $home->exportGetUrlAsPath->absolute->stringify, "exportWriteFile actually writes the file when creating exportPath"); + +# now make sure that it contains the correct content +eval { $content = WebGUI::Test->getPage($home, 'exportHtml_view', { user => WebGUI::User->new($session, 1) } ) }; +is(scalar $home->exportGetUrlAsPath->absolute->slurp, $content, "exportWriteFile puts the correct contents in exported home"); + + +# now that we know that creating the export directory works, let's make sure +# that creating it, when we have no permission to do so, throws an exception. + +# first, set the exportPath to a *sub*directory of $guid to ensure that it +# doesn't already exist, and then deny ourselves permissions to it. +my $unwritablePath = Path::Class::Dir->new($config->getWebguiRoot, '..', 'domains', $guid, $guid); +$config->set('exportPath', $unwritablePath->absolute->stringify); +chmod 0000, $guidPath; + +eval { $home->exportWriteFile() }; +$e = Exception::Class->caught(); +isa_ok($e, 'WebGUI::Error', "exportWriteFile throws if it can't create the export path"); +cmp_deeply( + $e, + methods( + error => "can't create exportPath $unwritablePath", + ), + "exportWriteFile throws if it can't create the export path" +); + +# the exception was thrown, but make sure that the file also wasn't written +ok(!-e $home->exportGetUrlAsPath->absolute->stringify, "exportWriteFile does not write the file when it can't create the exportPath"); + +# let's go a level deeper +# but reset the exportPath first +$config->set('exportPath', $guidPath->absolute->stringify); + +# and clean up the temp directory +chmod 0755, $guidPath; +$unwritablePath->remove; + +$session->http->{_http}->{noHeader} = 1; +eval { $gettingStarted->exportWriteFile() }; +is($@, '', "exportWriteFile works for getting_started"); + +# ensure that the file was actually written +ok(-e $gettingStarted->exportGetUrlAsPath->absolute->stringify, "exportWriteFile actually writes the getting_started file"); + +# verify it has the correct contents +eval { $content = WebGUI::Test->getPage($gettingStarted, 'exportHtml_view') }; +is(scalar $gettingStarted->exportGetUrlAsPath->absolute->slurp, $content, "exportWriteFile puts the correct contents in exported getting_started"); + +# and one more level. remove the export path to ensure directory creation keeps +# working. +$guidPath->rmtree; + +$session->http->{_http}->{noHeader} = 1; +$session->user( { userId => 1 } ); +eval { $grandChild->exportWriteFile() }; +is($@, '', "exportWriteFile works for grandchild"); + +# ensure that the file was written +ok(-e $grandChild->exportGetUrlAsPath->absolute->stringify, "exportWriteFile actually writes the grandchild file"); + +# finally, check its contents +eval { $content = WebGUI::Test->getPage($grandChild, 'exportHtml_view', { user => WebGUI::User->new($session, 1) }) }; +is(scalar $grandChild->exportGetUrlAsPath->absolute->slurp, $content, "exportWriteFile puts correct content in exported grandchild"); + +# test different extensions +$guidPath->rmtree; +$asset = WebGUI::Asset->new($session, 'ExportTest000000000001'); +$session->http->{_http}->{noHeader} = 1; +eval { $asset->exportWriteFile() }; +is($@, '', 'exportWriteFile for perl file works'); + +ok(-e $asset->exportGetUrlAsPath->absolute->stringify, "exportWriteFile actually writes the perl file"); + +eval { $content = WebGUI::Test->getPage($asset, 'exportHtml_view') }; +is(scalar $asset->exportGetUrlAsPath->absolute->slurp, $content, "exportWriteFile puts correct content in exported perl file"); + +$guidPath->rmtree; +$asset = WebGUI::Asset->new($session, 'ExportTest000000000002'); +eval { $asset->exportWriteFile() }; +is($@, '', 'exportWriteFile for plain file works'); + +ok(-e $asset->exportGetUrlAsPath->absolute->stringify, "exportWriteFile actuall writes the plain file"); + +eval { $content = WebGUI::Test->getPage($asset, 'exportHtml_view') }; +is(scalar $asset->exportGetUrlAsPath->absolute->slurp, $content, "exportWriteFile puts correct content in exported plain file"); + +$guidPath->rmtree; + +# next, make sure an exception is thrown if the user we're exporting as doesn't +# have permission to view the page that we want to export. by default, there's +# nothing actually in a stock WebGUI installation that any particular user +# isn't allowed to see. this means that we'll need to temporarily change the +# permissions on something. +$home->update( { groupIdView => 3 } ); # admins +$session->http->{_http}->{noHeader} = 1; +eval { $home->exportWriteFile() }; +$e = Exception::Class->caught(); +isa_ok($e, 'WebGUI::Error', "exportWriteFile throws when user can't view asset"); +cmp_deeply( + $e, + methods( + error => "user can't view asset at " . $home->getUrl . " to export it", + ), + "exportWriteFile throws when user can't view asset" +); + +# now that we're sure that it throws the correct exception, make sure there's +# no directory or file written +ok(!-e $home->exportGetUrlAsPath->absolute->stringify, "exportWriteFile doesn't write file when user can't view asset"); +ok(!-e $home->exportGetUrlAsPath->absolute->parent, "exportWriteFile doesn't write directory when user can't view asset"); + +# undo our viewing changes +$home->update( { groupIdView => 7 } ); # everyone +$guidPath->rmtree; + +#---------------------------------------------------------------------------- +# exportSymlinkExtrasUploads() + +# another class method. need to make sure it knows to check its first parameter +# for whether it's actually a WebGUI::Session. we don't need to fiddle with +# different paths or the permissions on them because if those paths are broken, +# other parts of the site will be utterly b0rked. + +# ensure it checks whether its first argument is a session object + +eval { WebGUI::Asset->exportSymlinkExtrasUploads }; +$e = Exception::Class->caught; +isa_ok($e, 'WebGUI::Error::InvalidObject', 'exportSymlinkExtrasUploads without session object throws'); +cmp_deeply( + $e, + methods( + error => 'first param to exportSymlinkExtrasUploads must be a WebGUI::Session', + ), + 'exportSymlinkExtrasUploads without session object throws', +); + +# call it with something that isn't a session +eval { WebGUI::Asset->exportSymlinkExtrasUploads('srsly? no wai!') }; +$e = Exception::Class->caught; +isa_ok($e, 'WebGUI::Error::InvalidObject', 'exportSymlinkExtrasUploads called with memetic parameter throws'); +cmp_deeply( + $e, + methods( + error => 'first param to exportSymlinkExtrasUploads must be a WebGUI::Session', + ), + 'exportSymlinkExtrasUploads called with memetic parameter throws', +); + + +# now test that it works as it should, when it should +$config->set('exportPath', $originalExportPath); +$exportPath = Path::Class::Dir->new($originalExportPath); +my $extrasPath = $config->get('extrasPath'); +my $extrasUrl = $config->get('extrasURL'); +my $uploadsPath = $config->get('uploadsPath'); +my $uploadsUrl = $config->get('uploadsURL'); + +eval { WebGUI::Asset->exportSymlinkExtrasUploads($session) }; + +# make sure it doesn't throw any exceptions +is($@, '', 'exportSymlinkExtrasUploads works when it should'); +my $extrasSymlink = Path::Class::File->new($exportPath, $extrasUrl); +my $uploadsSymlink = Path::Class::File->new($exportPath, $uploadsUrl); +ok(-e $extrasSymlink->absolute->stringify, "exportSymlinkExtrasUploads writes extras symlink"); +is($extrasPath, readlink $extrasSymlink->absolute->stringify, "exportSymlinkExtrasUploads extras symlink points to right place"); +ok(-e $uploadsSymlink->absolute->stringify, "exportSymlinkExtrasUploads writes uploads symlink"); +is($uploadsPath, readlink $uploadsSymlink->absolute->stringify, "exportSymlinkExtrasUploads uploads symlink points to right place"); + +#---------------------------------------------------------------------------- +# exportSymlinkRoot + +# This class method functions almost exactly the same as +# exportSymlinkExtrasUploads except that it puts a symlink in a diferent place. +# test that it verifies its parameter is a session object and that it does what +# it's supposed to do. + +eval { WebGUI::Asset->exportSymlinkRoot }; +$e = Exception::Class->caught; +isa_ok($e, 'WebGUI::Error::InvalidObject', 'exportSymlinkRoot without session object throws'); +cmp_deeply($e, + methods( + error => 'first param to exportSymlinkRoot must be a WebGUI::Session' + ), + 'exportSymlinkRoot without session object throws', +); + +# okay, so calling it without any parameters breaks. let's call it with +# something nonsensical +eval { WebGUI::Asset->exportSymlinkRoot('srsly! wai!') }; +$e = Exception::Class->caught; +isa_ok($e, 'WebGUI::Error::InvalidObject', 'exportSymlinkRoot called with memetic parameter throws'); +cmp_deeply($e, + methods( + error => 'first param to exportSymlinkRoot must be a WebGUI::Session' + ), + 'exportSymlinkRoot called with memetic parameter throws', +); + +# we need to make sure the code validates other parameters as well +eval { WebGUI::Asset->exportSymlinkRoot($session) }; +$e = Exception::Class->caught; +isa_ok($e, 'WebGUI::Error::InvalidParam', 'exportSymlinkRoot called without a default asset throws'); +cmp_deeply( + $e, + methods( + error => 'second param to exportSymlinkRoot must be the default asset', + param => undef, + ), + 'exportSymlinkRoot called without a default asset throws', +); + +# give it something not a default asset +eval { WebGUI::Asset->exportSymlinkRoot($session, "wai. can't be!") }; +$e = Exception::Class->caught; +isa_ok($e, 'WebGUI::Error::InvalidParam', 'exportSymlinkRoot called with memetic default asset throws'); +cmp_deeply( + $e, + methods( + error => 'second param to exportSymlinkRoot must be the default asset', + param => "wai. can't be!", + ), + 'exportSymlinkRoot called with memetic default asset throws', +); + +# it breaks when it's supposed to, so let's make sure it works when it's +# supposed to. first, leave out the index parameter to ensure it sets up the +# default correctly. +$home->exportWriteFile; +my $symlinkedRoot = Path::Class::File->new($exportPath, 'index.html'); +my $homePath = $home->exportGetUrlAsPath; +eval { WebGUI::Asset->exportSymlinkRoot($session, $home, '', 1) }; +is($@, '', 'exportSymlinkRoot works when it should'); +ok(-e $symlinkedRoot->stringify, 'exportSymlinkRoot sets up link correctly and supplies default index'); +is($homePath, readlink $symlinkedRoot->stringify, 'exportSymlinkRoot sets up link correctly and supplies default index'); +unlink $symlinkedRoot->stringify; + + +# give it an index and ensure it works +eval { WebGUI::Asset->exportSymlinkRoot($session, $home, 'index.html', 1) }; +is($@, '', 'exportSymlinkRoot works when it should'); +ok(-e $symlinkedRoot->stringify, 'exportSymlinkRoot sets up link correctly and supplies default index'); +is($homePath, readlink $symlinkedRoot->stringify, 'exportSymlinkRoot sets up link correctly and supplies default index'); +unlink $symlinkedRoot->stringify; + + +#---------------------------------------------------------------------------- +# exportGetDescendants() + +# clear these out now so that they don't interfere with the lineage tests +$asset = WebGUI::Asset->new($session, 'ExportTest000000000001'); +$asset->purge; +$asset = WebGUI::Asset->new($session, 'ExportTest000000000002'); +$asset->purge; + +$session->user( { userId => 1 } ); +my $descendants; +# next, make sure that we get the right list of assets to export. +my $homeDescendants = $home->getLineage( ['self', 'descendants'], { + endingLineageLength => $home->getLineageLength + 99, + orderByClause => 'assetData.url DESC', + } +); +$descendants = $home->exportGetDescendants( WebGUI::User->new($session, 1), 99 ); + +cmp_deeply($descendants, $homeDescendants, "exportGetDescendants returns correct data for home"); + +my $gsDescendants = $gettingStarted->getLineage( ['self', 'descendants'], { + endingLineageLength => $gettingStarted->getLineageLength + 99, + orderByClause => 'assetData.url DESC', + } +); +$descendants = $gettingStarted->exportGetDescendants( WebGUI::User->new($session, 1), 99 ); + +cmp_deeply($descendants, $gsDescendants, "exportGetDescendants returns correct data for getting-started"); + +my $gcDescendants = $grandChild->getLineage( ['self', 'descendants'], { + endingLineageLength => $grandChild->getLineageLength + 99, + orderByClause => 'assetData.url DESC', + } +); +$descendants = $grandChild->exportGetDescendants( WebGUI::User->new($session, 1), 99 ); + +cmp_deeply($descendants, $gcDescendants, "exportGetDescendants returns correct data for getting-started"); + +# finally, ensure that calling exportGetDescendants without a userID throws an exception. + +eval { $home->exportGetDescendants }; + +$e = Exception::Class->caught; +isa_ok($e, 'WebGUI::Error::InvalidObject', 'exportGetDescendants called without a user object throws'); +cmp_deeply( + $e, + methods( + expected => 'WebGUI::User', + got => '', + error => 'Need a WebGUI::User object', + param => undef, + ), + "exportGetDescendants called without a user object throws", +); + +# make sure calling exportGetDescendants without a depth throws an exception. + +eval { $home->exportGetDescendants( WebGUI::User->new($session, 1) ) }; +$e = Exception::Class->caught; +isa_ok($e, 'WebGUI::Error::InvalidParam', 'exportGetDescendants called without a depth throws'); +cmp_deeply( + $e, + methods( + error => 'Need a depth', + param => undef, + ), + "exportGetDescendants called without a depth throws", +); + +$session->user( { userId => 3 } ); + +#---------------------------------------------------------------------------- +# exportAsHtml + +# the big one. exportAsHtml is the central logic hub for all of the methods +# tested above. we don't need to test that the other methods work; that's what +# the other 70 tests above do. what we need to do is ensure that exportAsHtml: +# * processes its arguments correctly +# * calls the right methods in the right order +# * handles any exceptions +# * produces correct output +# * fails if it needs to fail +# in other words, we need to test that the ultimate results of calling +# exportAsHtml are what they should be, given the inputs we provide. +my (@createdFiles, @shouldExist, $success, $message); +my $exportPath = Path::Class::Dir->new($session->config->get('exportPath')); + +# first things first. let's make sure the code checks for the proper arguments. +# quiet is optional, so don't test that. userId is a bit smart and will take +# either a numeric userId or a real WebGUI::User object. everything else has a +# default. exportAsHtml is supposed to catch exceptions, not throw them, so +# we'll be testing the return values rather than for an exception. + +($success, $message) = $home->exportAsHtml; +is($success, 0, "exportAsHtml returns 0 when not given a userId"); +is($message, "need a userId parameter", "exportAsHtml returns correct message when not given a userId"); + +# omitting the userId works, so let's give it a bogus userId +($success, $message) = $home->exportAsHtml( { userId => ' perlDreamer is a 500 lb test mandating gorilla' } ); +is($success, 0, "exportAsHtml returns 0 when given a bogus (but nonetheless funny) userId"); +is($message, "' perlDreamer is a 500 lb test mandating gorilla' is not a valid userId", "exportAsHtml returns correct message when given a bogus (but nonetheless funny) userId"); + +# checking userId works, so check extrasUploadAction next. +($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99, extrasUploadAction => 'o hai' } ); +is($success, 0, "exportAsHtml returns 0 when given bogus, memetic extrasUploadAction parameter"); +is($message, "'o hai' is not a valid extrasUploadAction", "exportAsHtml returns 0 when given bogus, memetic extrasUploadAction parameter"); + +# rootUrlAction +($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99, rootUrlAction => 'NO U' } ); +is($success, 0, "exportAsHtml returns 0 when given bogus, memetic rootUrlAction parameter"); +is($message, "'NO U' is not a valid rootUrlAction", "exportAsHtml returns correct message when given bogus, memetic extrasUploadAction parameter"); + +# finally, depth +($success, $message) = $home->exportAsHtml( { userId => 3 } ); +is($success, 0, "exportAsHtml returns 0 when not given depth"); +is($message, "need a depth", "exportAsHtml returns correct message when not given a depth"); + +($success, $message) = $home->exportAsHtml( { userId => 3, depth => 'orly? yarly!' } ); +is($success, 0, "exportAsHtml returns 0 when given bogus, memetic depth"); +is($message, "'orly? yarly!' is not a valid depth", "exportAsHtml returns correct message when given bogus, memetic depth"); + +# next, let's make sure some simple exports work. export 'home', but clean up +# the exportPath first to make sure there are no residuals from the tests +# above. +$exportPath->rmtree; +($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99, quiet => 1 } ); + +# list of files that should exist. obtained by running previous known working +# export function on a full stock asset tree +@createdFiles = ( + [ qw/ getting_started getting-started index.html /], + [ qw/ getting_started getting-started-part2 index.html /], + [ qw/ getting_started index.html /], + [ qw/ home ad index.html /], + [ qw/ home ad2 index.html /], + [ qw/ home index.html /], + [ qw/ home key-benefits index.html /], + [ qw/ home welcome index.html /], + [ qw/ site_map index.html /], + [ qw/ site_map site_map index.html /], + [ qw/ tell_a_friend index.html /], + [ qw/ tell_a_friend tell_a_friend index.html /], + [ qw/ the_latest_news index.html /], + [ qw/ the_latest_news the_latest_news index.html /], + [ qw/ yns docs index.html /], + [ qw/ yns experts index.html /], + [ qw/ yns features index.html /], + [ qw/ yns hosting index.html /], + [ qw/ yns promotion index.html /], + [ qw/ yns style index.html /], + [ qw/ yns support index.html /], + [ qw/ yns translated index.html /], + [ qw/ your_next_step index.html /], +); + +# turn them into Path::Class::File objects +my @shouldExist = map { Path::Class::File->new($exportPath, @{$_})->absolute->stringify } @createdFiles; + +# ensure that the files that should exist do exist +my @doExist; +$exportPath->recurse( callback => sub { my $o = shift; $o->is_dir ? return : push @doExist, $o->absolute->stringify } ); +cmp_deeply(sort @shouldExist, sort @doExist, "exportAsHtml on home writes correct files"); +is($success, 1, "exportAsHtml on home returns true"); +like($message, qr/Exported 23 pages/, "exportAsHtml on home returns correct message"); + +$exportPath->rmtree; +@doExist = (); + +# previous tests ensure that the contents of the exported files are right. so +# let's go a level deeper and ensure that the right files are present. +($success, $message) = $gettingStarted->exportAsHtml( { userId => 3, depth => 99, quiet => 1 } ); +@createdFiles = ( + [ qw/ getting_started getting-started index.html /], + [ qw/ getting_started getting-started-part2 index.html /], + [ qw/ getting_started index.html /], + [ qw/ home ad2 index.html /], # I have no idea why but ad2 is a descendant of getting-started +); +@shouldExist = map { Path::Class::File->new($exportPath, @{$_})->absolute->stringify } @createdFiles; + +$exportPath->recurse( callback => sub { my $o = shift; $o->is_dir ? return : push @doExist, $o->absolute->stringify } ); +cmp_deeply(sort @shouldExist, sort @doExist, "exportAsHtml on getting-started writes correct files"); +is($success, 1, "exportAsHtml on getting-started returns true"); +like($message, qr/Exported 4 pages/, "exportAsHtml on getting-started returns correct message"); + +$exportPath->rmtree; +@doExist = (); + +# test the grandchild. +($success, $message) = $grandChild->exportAsHtml( { userId => 3, depth => 99, quiet => 1 } ); +@createdFiles = ( + [ qw/ getting_started getting-started index.html /], +); + +@shouldExist = map { Path::Class::File->new($exportPath, @{$_})->absolute->stringify } @createdFiles; + +$exportPath->recurse( callback => sub { my $o = shift; $o->is_dir ? return : push @doExist, $o->absolute->stringify } ); +cmp_deeply(sort @shouldExist, sort @doExist, "exportAsHtml on grandchild writes correct files"); +is($success, 1, "exportAsHtml on grandchild returns true"); +like($message, qr/Exported 1 pages/, "exportAsHtml on grandchild returns correct message"); + +$exportPath->rmtree; +@doExist = (); + +# fiddle with the isExportable setting and make sure appropriate files are +# written +$home->update({ isExportable => 0 }); +($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99, quiet => 1 } ); + +@shouldExist = (); +is(@shouldExist, @doExist, "exportAsHtml on nonexportable home doesn't write anything"); +is($success, 1, "exportAsHtml on nonexportable home returns true (but doesn't do anything)"); +like($message, qr/Exported 0 pages/, "exportAsHtml on nonexportable home returns correct message"); + +# restore the original setting +$home->update({ isExportable => 1 }); + +# go a level deeper + +# shouldn't be necessary if the tests pass, but be nice and clean up after ourselves +$exportPath->rmtree; + +@doExist = (); +$gettingStarted->update({ isExportable => 0 }); + +($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99, quiet => 1 } ); + +# since getting-started isn't exportable, it shouldn't be written. remove it +# and its descendants from the list. +@createdFiles = ( + [ qw/ home ad index.html /], + #[ qw/ home ad2 index.html /], # I have no idea why but ad2 is a descendant of getting-started + [ qw/ home index.html /], + [ qw/ home key-benefits index.html /], + [ qw/ home welcome index.html /], + [ qw/ site_map index.html /], + [ qw/ site_map site_map index.html /], + [ qw/ tell_a_friend index.html /], + [ qw/ tell_a_friend tell_a_friend index.html /], + [ qw/ the_latest_news index.html /], + [ qw/ the_latest_news the_latest_news index.html /], + [ qw/ yns docs index.html /], + [ qw/ yns experts index.html /], + [ qw/ yns features index.html /], + [ qw/ yns hosting index.html /], + [ qw/ yns promotion index.html /], + [ qw/ yns style index.html /], + [ qw/ yns support index.html /], + [ qw/ yns translated index.html /], + [ qw/ your_next_step index.html /], +); +@shouldExist = map { Path::Class::File->new($exportPath, @{$_})->absolute->stringify } @createdFiles; + +$exportPath->recurse( callback => sub { my $o = shift; $o->is_dir ? return : push @doExist, $o->absolute->stringify } ); +cmp_deeply(sort @shouldExist, sort @doExist, "exportAsHtml on home with non-exportable getting-started writes correct files"); +is($success, 1, "exportAsHtml on home with non-exportable getting-started returns true"); +like($message, qr/Exported 19 pages/, "exportAsHtml on home with non-exportable getting-started returns correct message"); + +# restore the original setting +$gettingStarted->update({ isExportable => 1 }); + +$exportPath->rmtree; +@doExist = (); + +# now that we're sure that it works when everything is set up properly, let's +# test the code under inclement circumstances. let's cover each method that +# exportAsHtml calls in turn. we'll make sure it catches each exception that we +# can generate here. exceptions shouldn't propagate to the www_ methods. they +# should be caught before that point and a message returned to the user. the +# best way to do these is to mimic the order that they're tested above. we +# can't test the invalid argument exceptions, though, because the environment +# for those tests is the actual code of the exportAsHtml method. however, +# everything that's external to the code of the method itself we can test, like +# an unset exportPath. we'll test a couple of things. note that these +# exceptions should be *caught* by exportAsHtml, so the code needs to live. +# also, we need to test that appropriate status messages based on those +# exceptions are returned to the calling method. given the above, we'll test +# the following situations and verify that the following things occur properly: +# checkExportPath: +# 1. lack of defined exportPath +# 2. inaccessible exportPath +# 3. exportPath is a file, not a directory +# 4. can't create path for whatever reason +# exportCheckExportable: +# doesn't throw exceptions +# exportWriteFile: +# 1. user can't view asset +# exportGetDescendants: +# doesn't throw exceptions we can test (they're all method usage-related) + +# let's start with an invalid exportPath +$config->delete('exportPath'); + +# undefined exportPath +eval { ($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99 } ) }; +is($@, '', "exportAsHtml catches undefined exportPath exception"); +is($success, 0, "exportAsHtml returns 0 for undefined exportPath"); +is($message, 'exportPath must be defined and not ""', "exportAsHtml returns correct message for undefined exportPath"); + +# inaccessible exportPath +$config->set('exportPath', Path::Class::Dir->new('')->stringify); + +eval { ($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99 } ) }; +is($@, '', "exportAsHtml catches inaccessible exportPath "); +is($success, 0, "exportAsHtml returns 0 for inaccessible exportPath"); +is($message, "can't access " . Path::Class::Dir->new('')->stringify, "exportAsHtml returns correct message for inaccessible exportPath"); + +# exportPath is a file, not a directory +$config->set('exportPath', $exportPathFile); + +eval { ($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99 } ) }; +is($@, '', "exportAsHtml catches exportPath is file exception"); +is($success, 0, "exportAsHtml returns 0 if exportPath is a file"); +is($message, "$exportPathFile isn't a directory", "exportAsHtml returns correct message if exportPath is a file"); + +# can't create export path +chmod 0000, $tempDirectory; +$config->set('exportPath', $inaccessibleDirectory->stringify); + +eval { ($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99 } ) }; +is($@, '', "exportAsHtml catches uncreatable exportPath exception"); +is($success, 0, "exportAsHtml returns 0 for uncreatable exportPath"); +is($message, "can't create exportPath $inaccessibleDirectory", "exportAsHtml returns correct message for uncreatable exportPath"); + +# user can't view asset +$home->update( { groupIdView => 3 } ); +$session->http->{_http}->{noHeader} = 1; + +chmod 0755, $tempDirectory; +eval { ($success, $message) = $home->exportAsHtml( { userId => 1, depth => 99 } ) }; +is($@, '', "exportAsHtml catches unviewable asset exception"); +is($success, 0, "exportAsHtml returns 0 for unviewable asset"); +is($message, "can't view asset at URL /home", "exportAsHtml returns correct message for unviewable asset"); + +# fix viewing the asset +$home->update( { groupIdView => 7 } ); + +# the "can't write file" exceptions for exportWriteFile are largely related to +# the exportPath being broken somehow. That's already been tested. next, let's +# make sure symlinking works. start with extrasUploadAction. no use checking +# for valid paths and URLs for these values in the config file. the site would +# be horridly, totally broken if they were incorrect. assume that they're +# valid. +$config->set('exportPath', $originalExportPath); +$exportPath = Path::Class::Dir->new($originalExportPath); +$extrasPath = $config->get('extrasPath'); +$extrasUrl = $config->get('extrasURL'); +$uploadsPath = $config->get('uploadsPath'); +$uploadsUrl = $config->get('uploadsURL'); + +$exportPath->rmtree; + +($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99, extrasUploadAction => 'symlink', quiet => 1 } ); +$extrasSymlink = Path::Class::File->new($exportPath, $extrasUrl); +$uploadsSymlink = Path::Class::File->new($exportPath, $uploadsUrl); +is($success, 1, "exportAsHtml when linking extras and uploads returns true"); +like($message, qr/Exported 23 pages/, "exportAsHtml when linking extras and uploads returns correct message"); +ok(-e $extrasSymlink->absolute->stringify, "exportAsHtml writes extras symlink"); +is($extrasPath, readlink $extrasSymlink->absolute->stringify, "exportAsHtml extras symlink points to right place"); +ok(-e $uploadsSymlink->absolute->stringify, "exportAsHtml writes uploads symlink"); +is($uploadsPath, readlink $uploadsSymlink->absolute->stringify, "exportAsHtml uploads symlink points to right place"); + +# next, make sure the root URL symlinking works. +($success, $message) = $home->exportAsHtml( { userId => 3, depth => 99, rootUrlAction => 'symlink', quiet => 1 } ); +my $rootUrlSymlink = Path::Class::File->new($exportPath, 'index.html'); +is($success, 1, 'exportAsHtml when linking root URL returns true'); +like($message, qr/Exported 23 pages/, "exportAsHtml when linking root URL returns correct message"); +ok(-e $rootUrlSymlink->absolute->stringify, "exportAsHtml writes root URL symlink"); +is($home->exportGetUrlAsPath->absolute->stringify, readlink $rootUrlSymlink->absolute->stringify, "exportAsHtml root URL symlink points to right place"); + + +#---------------------------------------------------------------------------- +# Cleanup +END { + + # remove $tempDirectory since it now exists in the filesystem + rmtree($tempDirectory); + + # restore the original exportPath setting, now that we're done testing + # exportCheckPath. + $session->config->set('exportPath', $originalExportPath); + + $versionTag->rollback(); + + # clean out the files written to the $guid export directory. + rmtree($guid); + + # make sure people can view /home + $home->update( { groupIdView => 7 } ); # everyone } - -## Test something else - -