diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index 32e79442b..f0732a868 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -10,6 +10,8 @@ - added Google Maps asset - added a way for authors to prevent debugging output on page requests - added --noprompt option to testEnvironment.pl to automatically install modules + - added FilePump, a way to reduce Yslow scores by minimizing JS, CSS and the + number of files transferred. 7.7.6 - Added mobile style template. If enabled in settings, will serve alternate style templates diff --git a/docs/upgrades/upgrade_7.7.6-7.7.7.pl b/docs/upgrades/upgrade_7.7.6-7.7.7.pl index 057779115..929304d6f 100644 --- a/docs/upgrades/upgrade_7.7.6-7.7.7.pl +++ b/docs/upgrades/upgrade_7.7.6-7.7.7.pl @@ -22,7 +22,7 @@ use Getopt::Long; use WebGUI::Session; use WebGUI::Storage; use WebGUI::Asset; - +use WebGUI::FilePump::Bundle; my $toVersion = '7.7.7'; my $quiet; # this line required @@ -36,6 +36,9 @@ addRedirectAfterLoginUrlToSettings( $session ); addSurveyTestResultsTemplateColumn( $session ); fixSMSUserProfileI18N($session); addMapAsset( $session ); +installFilePumpHandler($session); +installFilePumpTable($session); +installFilePumpAdminGroup($session); finish($session); # this line required @@ -107,6 +110,48 @@ sub addSurveyTestResultsTemplateColumn { print "Done\n" unless $quiet; } +#---------------------------------------------------------------------------- +sub installFilePumpAdminGroup { + my $session = shift; + print "\tAdding FilePump admin group setting... \n" unless $quiet; + ##Content Handler + #if (! $session->setting->has('groupIdAdminFilePump')) { + $session->setting->add('groupIdAdminFilePump','8'); + print "\tAdded FilePump admin group ... \n" unless $quiet; + #} + print "Done.\n" unless $quiet; +} + +#---------------------------------------------------------------------------- +sub installFilePumpHandler { + my $session = shift; + print "\tAdding FilePump content handler... \n" unless $quiet; + ##Content Handler + my $contentHandlers = $session->config->get('contentHandlers'); + $session->config->addToArray('contentHandlers', 'WebGUI::Content::FilePump'); + $session->config->addToHash( 'macros', { FilePump => 'FilePump' }); + + ##Admin Console + $session->config->addToHash('adminConsole', 'filePump', { + "icon" => "filePump.png", + "groupSetting" => "groupIdAdminFilePump", + "uiLevel" => 5, + "url" => "^PageUrl(\"\",op=filePump);", + "title" => "^International(File Pump,FilePump);" + }); + ##Setting for custom group + print "Done.\n" unless $quiet; +} + +#---------------------------------------------------------------------------- +sub installFilePumpTable { + my $session = shift; + print "\tAdding FilePump database table via CRUD... \n" unless $quiet; + WebGUI::FilePump::Bundle->crud_createTable($session); + print "Done.\n" unless $quiet; +} + + #---------------------------------------------------------------------------- # Add the map asset sub addMapAsset { diff --git a/etc/WebGUI.conf.original b/etc/WebGUI.conf.original index 7bbcdb2af..1b7d0c796 100644 --- a/etc/WebGUI.conf.original +++ b/etc/WebGUI.conf.original @@ -402,6 +402,13 @@ "url" : "^PageUrl(\"\",op=manageCache);", "title" : "^International(manage cache,WebGUI);", "groupSetting" : "groupIdAdminCache" + }, + "filePump" : { + "icon" : "filePump.png", + "uiLevel" : 5, + "url" : "^PageUrl(\"\",op=filePump);", + "title" : "^International(File Pump,FilePump);", + "groupSetting" : "groupIdAdminFilePump" } }, @@ -778,6 +785,7 @@ "e" : "e_companyEmail", "Extras" : "Extras", "FetchMimeType" : "FetchMimeType", + "FilePump" : "FilePump", "FileUrl" : "FileUrl", "GroupAdd" : "GroupAdd", "GroupDelete" : "GroupDelete", diff --git a/lib/WebGUI/Content/FilePump.pm b/lib/WebGUI/Content/FilePump.pm new file mode 100644 index 000000000..5964bfef0 --- /dev/null +++ b/lib/WebGUI/Content/FilePump.pm @@ -0,0 +1,52 @@ +package WebGUI::Content::FilePump; + +use strict; +use WebGUI::AdminConsole; +use WebGUI::Exception; +use WebGUI::FilePump::Admin; + +=head1 NAME + +Package WebGUI::Content::FilePump + +=head1 DESCRIPTION + +Handle all requests for building and editing FilePump bundles + +=head1 SYNOPSIS + + use WebGUI::Content::FilePump; + my $output = WebGUI::Content::FilePump::handler($session); + +=head1 SUBROUTINES + +These subroutines are available from this package: + +=cut + +#------------------------------------------------------------------- + +=head2 handler ( session ) + +The content handler for this package. + +=cut + +sub handler { + my ($session) = @_; + my $output = undef; + return undef unless $session->form->get('op') eq 'filePump'; + my $func = $session->form->get( 'func' ) + ? 'www_' . $session->form->get( 'func' ) + : 'www_manage' + ; + if ($func ne "www_" && (my $sub = WebGUI::FilePump::Admin->can($func))) { + $output = $sub->($session); + } + else { + WebGUI::Error::MethodNotFound->throw(error=>"Couldn't call non-existant function $func inside FilePump", method=>$func); + } + return $output; +} + +1; diff --git a/lib/WebGUI/FilePump/Admin.pm b/lib/WebGUI/FilePump/Admin.pm new file mode 100644 index 000000000..de90f2adf --- /dev/null +++ b/lib/WebGUI/FilePump/Admin.pm @@ -0,0 +1,371 @@ +package WebGUI::FilePump::Admin; + +use strict; +use Tie::IxHash; +use WebGUI::AdminConsole; +use WebGUI::HTMLForm; +use WebGUI::International; +use WebGUI::Pluggable; +use WebGUI::Utility; +use WebGUI::FilePump::Bundle; + +=head1 NAME + +Package WebGUI::FilePump::Admin + +=head1 DESCRIPTION + +Web interface for making, building, and editing FilePump bundles. + +=cut + +#---------------------------------------------------------------------------- + +=head2 canView ( session [, user] ) + +Returns true if the user can administrate this operation. user defaults to +the current user. + +=cut + +sub canView { + my $session = shift; + my $user = shift || $session->user; + return $user->isInGroup( $session->setting->get('groupIdAdminFilePump') ); +} + +#------------------------------------------------------------------- + +=head2 www_addBundle ( ) + +Displays a form to add a bundle. + +Note, we do not allow bundle names to be edited. This is why. The directory to a bundle is based on +the bundle name, and the time stamp of the last build. If you change the name, then you have a few +options. + +1) You delete the build directory with the old name, which will break every macro which references it. + +2) You leave it there, which means that they accumulate with time since they can't every be deleted because +you don't know the old name. + +In short, this really means that instead of an option to edit the name, it needs a copy function. When you +copy the bundle, it asks you what you want for a new name, and it is supplied by the user at that time. + +=cut + +sub www_addBundle { + my ($session) = @_; + return $session->privilege->insufficient() unless canView($session); + + ##Build the form + my $i18n = WebGUI::International->new($session, 'FilePump'); + my $form = WebGUI::HTMLForm->new($session); + $form->hidden( name=>"op", value=>"filePump"); + $form->hidden( name=>"func", value=>"addBundleSave"); + $form->text( + name => 'bundleName', + defaultValue => $i18n->get('new bundle'), + label => $i18n->get('bundle name'), + hoverHelp => $i18n->get('bundle name help'), + ); + $form->submit; + + my $ac = WebGUI::AdminConsole->new($session,'filePump'); + return $ac->render($form->print, $i18n->get('Add Bundle')); +} + +#------------------------------------------------------------------- + +=head2 www_addBundleSave ( ) + +Saves the results of www_addBundle(). + +=cut + +sub www_addBundleSave { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $form = $session->form; + my $bundleName = $form->get('bundleName'); + my $bundle = WebGUI::FilePump::Bundle->create($session, { + bundleName => $bundleName, + lastModified => time(), + }); + return www_manage($session); +} + +#------------------------------------------------------------------- + +=head2 www_addFile ( ) + +Processes the form to add a file to a bundle. + +Form variables used: + +=item uri + +The URI to add. + +=item type + +The type of file to add. + +=item bundleId + +The GUID of the bundle to add a file to. + +=cut + +sub www_addFile { + my ($session) = @_; + return $session->privilege->insufficient() unless canView($session); + my $form = $session->form; + my $bundleId = $form->get('bundleId'); + my $bundle = WebGUI::FilePump::Bundle->new($session, $bundleId); + return www_editBundle($session) unless $bundle; + + my $type = $form->get('type'); + my $uri = $form->get('uri'); + + my (undef, $error) = $bundle->addFile($type, $uri); + + return www_editBundle($session, $error); +} + +#------------------------------------------------------------------- + +=head2 www_buildBundle ( ) + +Builds a bundle, identified by the form variable, bundleId. + +=cut + +sub www_buildBundle { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $bundle = WebGUI::FilePump::Bundle->new($session, $session->form->get("bundleId")); + return www_editBundle($session) unless $bundle; + my ($code, $error) = $bundle->build; + if ($error) { + my $i18n = WebGUI::International->new($session, 'FilePump'); + $error = sprintf $i18n->get('build error'), $error; + } + return www_editBundle($session, $error); +} + +#------------------------------------------------------------------- + +=head2 www_deleteBundle ( ) + +Deletes a bundle, identified by the form variable, bundleId. + +=cut + +sub www_deleteBundle { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $bundle = WebGUI::FilePump::Bundle->new($session, $session->form->get("bundleId")); + if (defined $bundle) { + $bundle->delete; + } + return www_manage($session); +} + +#------------------------------------------------------------------ + +=head2 www_deleteFile ( session ) + +Deletes a file from it's bundle. The kind of file is set by the form variable filetype, +the id of the bundle is bundleId, and the id of the file to move is fileId. + +=head3 session + +A reference to the current session. + +=cut + +sub www_deleteFile { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $form = $session->form; + my $bundle = WebGUI::FilePump::Bundle->new($session, $form->get("bundleId")); + return www_editBundle($session) unless $bundle; + $bundle->deleteFile($form->get('fileType'), $form->get('fileId')); + return www_editBundle($session); +} + +#------------------------------------------------------------------ + +=head2 www_demoteFile ( session ) + +Moves a bundle file down one position. The kind of file is set by the form variable fileType, +the id of the bundle is bundleId, and the id of the file to move is fileId. + +=head3 session + +A reference to the current session. + +=cut + +sub www_demoteFile { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $form = $session->form; + my $bundle = WebGUI::FilePump::Bundle->new($session, $form->get("bundleId")); + return www_editBundle($session) unless $bundle; + my $type = $form->get('fileType'); + my $fileId = $form->get('fileId'); + $bundle->moveFileDown($type, $fileId); + return www_editBundle($session); +} + +#------------------------------------------------------------------ + +=head2 www_editBundle ( session ) + +Interface for managing URIs in a bundle, given by the form param bundleId. Add, delete, +promote and demote are supported for all three file types. + +=head3 session + +A reference to the current session. + +=cut + +sub www_editBundle { + my ($session, $error) = @_; + return $session->privilege->insufficient() unless canView($session); + my $bundleId = $session->form->get("bundleId"); + my $bundle = WebGUI::FilePump::Bundle->new($session, $bundleId); + return www_addBundle($session) unless $bundle; + my $i18n = WebGUI::International->new($session, 'FilePump'); + if ($error) { + $error = qq|
$error
\n|; + } + my $tableStub = <%s + + +%s +
 URI%s
+

%s

+EOTABLE + my $output = ''; + my $dt = $session->datetime; + my $url = $session->url; + my $lastModifiedi18n = $i18n->get('last modified'); + foreach my $fileType (qw/jsFiles cssFiles/) { + my $type = $fileType eq 'jsFiles' ? 'JS' + : $fileType eq 'cssFiles' ? 'CSS' + : 'OTHER'; + my $form = WebGUI::Form::formHeader($session, { + action => $url->gateway($url->getRequestedUrl,'op=filePump;func=addFile;type='.$type.';bundleId='.$bundleId), + }) + . WebGUI::Form::text($session, { + name => 'uri', + size => 45, + }) + . WebGUI::Form::submit($session) + . WebGUI::Form::formFooter() + ; + + my $rows = ''; + my $files = $bundle->get($fileType); + foreach my $file (@{ $files }) { + my $urlFrag = 'bundleId='.$bundleId.';fileType='.$type.';fileId='.$file->{fileId}; + $rows .= sprintf '%s%s%s', + $session->icon->delete( 'op=filePump;func=deleteFile;' . $urlFrag). + $session->icon->moveUp( 'op=filePump;func=promoteFile;' . $urlFrag). + $session->icon->moveDown( 'op=filePump;func=demoteFile;' . $urlFrag) , + $file->{uri}, + $file->{lastModified} ? $dt->epochToHuman($file->{lastModified}) : ' ' + ; + } + $output .= sprintf $tableStub, $i18n->get($fileType), $lastModifiedi18n, $rows, $form; + } + + my $ac = WebGUI::AdminConsole->new($session,'filePump'); + $ac->addSubmenuItem($session->url->page('op=filePump;'), $i18n->get('list bundles')); + $ac->addSubmenuItem($session->url->page('op=filePump;func=addBundle'), $i18n->get('add a bundle')); + $ac->addSubmenuItem($session->url->page('op=filePump;func=buildBundle;bundleId='.$bundleId), $i18n->get('build this bundle')); + return $ac->render($error.$output, 'File Pump'); +} + +#------------------------------------------------------------------ + +=head2 www_promoteFile ( session ) + +Moves a bundle file up one position. The kind of file is set by the form variable fileType, +the id of the bundle is bundleId, and the id of the file to move is fileId. + +=head3 session + +A reference to the current session. + +=cut + +sub www_promoteFile { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $form = $session->form; + my $bundle = WebGUI::FilePump::Bundle->new($session, $session->form->get("bundleId")); + return www_editBundle($session) unless $bundle; + my $type = $form->get('fileType'); + my $fileId = $form->get('fileId'); + $bundle->moveFileUp($type, $fileId); + return www_editBundle($session); +} + +#------------------------------------------------------------------- + +=head2 www_manage ( session ) + +Display a list of available bundles. Provide ways to add, edit and delete them. + +=head3 $session + +A WebGUI session object. + +=cut + +sub www_manage { + my $session = shift; + return $session->privilege->insufficient() unless canView($session); + my $i18n = WebGUI::International->new($session, 'FilePump'); + my $error = shift; + my $rows = ''; + my $dt = $session->datetime; + my $url = $session->url; + my $getABundle = WebGUI::FilePump::Bundle->getAllIterator($session,{ orderBy => 'bundleName' } ); + my $notYet = $i18n->get('not yet'); + while (my $bundle = $getABundle->()) { + my $lastModified = $bundle->get('lastModified'); + my $lastBuild = $bundle->get('lastBuild'); + my $build = ''; + if ($lastModified > $lastBuild) { + $build = sprintf q| (%s)|, + $url->gateway($url->getRequestedUrl,'op=filePump;func=buildBundle;bundleId='.$bundle->getId), + $i18n->get('build'); + } + $rows .= sprintf '%s%s%s%s', + $session->icon->delete('op=filePump;func=deleteBundle;bundleId='.$bundle->getId), + $url->gateway($url->getRequestedUrl,'op=filePump;func=editBundle;bundleId='.$bundle->getId), + $bundle->get('bundleName'), + $bundle->get('lastModified') ? $dt->epochToHuman($lastModified) : $notYet, + $bundle->get('lastBuild') ? $dt->epochToHuman($lastBuild).$build : $notYet, + ; + } + my $output = sprintf <get('bundle name'), $i18n->get('last modified'), $i18n->get('last build'), $rows; + + +%s +
 %s%s%s
+EOHTML + my $ac = WebGUI::AdminConsole->new($session,'filePump'); + $ac->addSubmenuItem($session->url->page('op=filePump;func=addBundle'), $i18n->get('add a bundle')); + return $ac->render($error.$output, 'File Pump'); +} + + +1; diff --git a/lib/WebGUI/FilePump/Bundle.pm b/lib/WebGUI/FilePump/Bundle.pm new file mode 100644 index 000000000..456fc7f8c --- /dev/null +++ b/lib/WebGUI/FilePump/Bundle.pm @@ -0,0 +1,837 @@ +package WebGUI::FilePump::Bundle; + +use base qw/WebGUI::Crud/; +use WebGUI::International; +use WebGUI::Utility; +use URI; +use Path::Class; +use CSS::Minifier::XS; +use JavaScript::Minifier::XS; +use LWP; +use Data::Dumper; + +#------------------------------------------------------------------- + +=head2 addFile ( $type, $uri ) + +Adds a file of the requested type to the bundle. Returns 1 if the add was successful. +Otherwise, returns 0 and an error message as to why it was not successful. + +=head3 $type + +If $type is JS, it adds it to the javascript part of the bundle. If it is +CSS, it adds it to the CSS part of the bundle. OTHER is used for all other +types of files. + +=head3 $uri + +A URI to the new file to add. If the URI already exists in that part of the bundle, +it will return 0 and an error message. + +=cut + +sub addFile { + my ($self, $type, $uri) = @_; + return 0, 'Illegal type' unless WebGUI::Utility::isIn($type, 'JS', 'CSS', 'OTHER'); + return 0, 'No URI' unless $uri; + my $collateralType = $type eq 'JS' ? 'jsFiles' + : $type eq 'CSS' ? 'cssFiles' + : 'otherFiles'; + my $files = $self->get($collateralType); + my $uriExists = $self->getCollateralDataIndex($files, 'uri', $uri) != -1 ? 1 : 0; + return 0, 'Duplicate URI' if $uriExists; + $self->setCollateral( + $collateralType, + 'fileId', + 'new', + { + uri => $uri, + lastModified => 0, + }, + ); + $self->update({lastModified => time()}); + return 1; +} + +#------------------------------------------------------------------- + +=head2 build ( ) + +build goes through and fetches all files referenced in all URIs stored for +this bundle. It downloads them, stores their modification time for future +checks, and then does special processing, depending on the type of file. + +Javascript files are concatenated together in order, and minimized. The +resulting data is stored in the filepump area under the uploads directory +with the name bundleName.timestamp/bundleName.js + +CSS files are handled likewise, except that the name is bundleName.timestamp/bundleName.css. + +Other files are copied from their current location into the timestamped bundle directory. + +Older timestamped build directories are removed. + +If the build is successful, it will return 1. Otherwise, if problems +occur during the build, then the old build directory is not affected and +the method returns 0, along with an error message. + +=cut + +sub build { + my ($self) = @_; + my $newBuild = time(); + my $originalBuild = $self->get('lastBuild'); + + ##Whole lot of building + my $error = undef; + + ##JavaScript first + my $jsFiles = $self->get('jsFiles'); + my $concatenatedJS = ''; + JSFILE: foreach my $jsFile (@{ $jsFiles }) { + my $uri = $jsFile->{uri}; + my $results = $self->fetch($uri); + if (! $results->{content}) { + $error = $uri; + last JSFILE; + } + $concatenatedJS .= $results->{content}; + $jsFile->{lastModified} = $results->{lastModified}; + } + return (0, $error) if ($error); + + ##CSS next + my $cssFiles = $self->get('cssFiles'); + my $concatenatedCSS = ''; + CSSFILE: foreach my $cssFile (@{ $cssFiles }) { + my $uri = $cssFile->{uri}; + my $results = $self->fetch($uri); + if (! $results->{content}) { + $error = $uri; + last CSSFILE; + } + $concatenatedCSS .= $results->{content}; + $cssFile->{lastModified} = $results->{lastModified}; + } + + return (0, $error) if ($error); + + ##Create the new build directory + my $newDir = $self->getPathClassDir($newBuild); + my $mkpathErrors; + my $dirsCreated = $newDir->mkpath({ errors => $mkpathErrors }); + if (! $dirsCreated) { + $newDir->rmtree; + my $errorMessages = join "\n", @{ $mkpathErrors }; + return (0, $errorMessages); + } + + ##Minimize files, and write them out. + + my $minimizedJS = JavaScript::Minifier::XS::minify($concatenatedJS); + undef $concatenatedJS; + + my $minimizedCSS = CSS::Minifier::XS::minify($concatenatedCSS); + undef $concatenatedCSS; + + my $flatJsFile = $newDir->file($self->bundleUrl . '.js'); + my $jsFH = $flatJsFile->open('>'); + print $jsFH $minimizedJS; + close $jsFH; + + my $flatCssFile = $newDir->file($self->bundleUrl . '.css'); + my $cssFH = $flatCssFile->open('>'); + print $cssFH $minimizedCSS; + close $cssFH; + + ##Delete the old build directory and update myself with the new data. + $self->deleteBuild(); + $self->update({ + jsFiles => $jsFiles, + cssFiles => $cssFiles, + lastBuild => $newBuild, + }); + return 1; +} + +#------------------------------------------------------------------- + +=head2 crud_definition + +WebGUI::Crud definition for this class. + +=head3 tableName + +filePumpBundle + +=head3 tableKey + +bundleId + +=head3 sequenceKey + +None. Bundles have no sequence amongst themselves. + +=head3 properties + +=head4 bundleName + +The name of a bundle + +=head4 lastBuild + +The date the bundle was last built. This is used to generate the name of the bundled files +for this bundle. + +=head4 lastModified + +The date the bundle was last modified. With this, and the lastBuild date, you can determine +which bundles need to be rebuilt. + +=head4 jsFiles, cssFiles, otherFiles + +JSON blobs with files attached to the bundle. js = javascript, css = Cascading Style Sheets, other +means anything else. + +=cut + +sub crud_definition { + my ($class, $session) = @_; + my $definition = $class->SUPER::crud_definition($session); + my $i18n = WebGUI::International->new($session, 'FilePump'); + $definition->{tableName} = 'filePumpBundle'; + $definition->{tableKey} = 'bundleId'; + $definition->{sequenceKey} = ''; + my $properties = $definition->{properties}; + $properties->{bundleName} = { + fieldType => 'text', + defaultValue => $i18n->get('new bundle'), + }; + $properties->{lastModified} = { + fieldType => 'integer', + defaultValue => 0, + }; + $properties->{lastBuild} = { + fieldType => 'integer', + defaultValue => 0, + }; + $properties->{jsFiles} = { + fieldType => 'textarea', + defaultValue => [], + serialize => 1, + }; + $properties->{cssFiles} = { + fieldType => 'textarea', + defaultValue => [], + serialize => 1, + }; + $properties->{otherFiles} = { + fieldType => 'textarea', + defaultValue => [], + serialize => 1, + }; + return $definition; +} + +#------------------------------------------------------------------- + +=head2 delete ( ) + +Extend the method from WebGUI::Crud to handle deleting the locally stored +files. + +=cut + +sub delete { + my ($self) = @_; + $self->deleteBuild; + return $self->SUPER::delete; +} + +#------------------------------------------------------------------- + +=head2 deleteBuild ( ) + +Delete the build as specified by the Bundle's current lastBuild timestamp; + +=cut + +sub deleteBuild { + my ($self) = @_; + my $bundleDir = $self->getPathClassDir(); + $bundleDir->rmtree(); +} + +#------------------------------------------------------------------- + +=head2 deleteCollateral ( tableName, keyName, keyValue ) + +Deletes a row of collateral data. + +=head3 tableName + +The name of the table you wish to delete the data from. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to delete. + +=cut + +sub deleteCollateral { + my $self = shift; + my $tableName = shift; + my $keyName = shift; + my $keyValue = shift; + my $table = $self->get($tableName); + my $index = $self->getCollateralDataIndex($table, $keyName, $keyValue); + return if $index == -1; + splice @{ $table }, $index, 1; + $self->update({ $tableName => $table }); +} + +#------------------------------------------------------------------- + +=head2 deleteFiles ( $type ) + +Deletes all files of the requested type. + +=head3 $type + +If $type is JS, it deletes it from the javascript part of the bundle. If it is +CSS, it deletes it from the CSS part of the bundle. OTHER is used for all other +types of files. + +=cut + +sub deleteFiles { + my ($self, $type) = @_; + return 0, 'Illegal type' unless WebGUI::Utility::isIn($type, 'JS', 'CSS', 'OTHER'); + my $collateralType = $type eq 'JS' ? 'jsFiles' + : $type eq 'CSS' ? 'cssFiles' + : 'otherFiles'; + $self->update({$collateralType => []}); + return 1; +} + +#------------------------------------------------------------------- + +=head2 deleteFile ( $type, $fileId ) + +Deletes a file of the requested type from the bundle. + +=head3 $type + +If $type is JS, it deletes it from the javascript part of the bundle. If it is +CSS, it deletes it from the CSS part of the bundle. OTHER is used for all other +types of files. + +=head3 $fileId + +The unique collateral GUID to delete from the bundle. + +=cut + +sub deleteFile { + my ($self, $type, $fileId) = @_; + return 0, 'Illegal type' unless WebGUI::Utility::isIn($type, 'JS', 'CSS', 'OTHER'); + return 0, 'No fileId' unless $fileId; + my $collateralType = $type eq 'JS' ? 'jsFiles' + : $type eq 'CSS' ? 'cssFiles' + : 'otherFiles'; + $self->deleteCollateral( + $collateralType, + 'fileId', + $fileId, + ); + $self->update({lastModified => time()}); + return 1; +} + +#------------------------------------------------------------------- + +=head2 fetch ( $uri ) + +Based on the scheme of the URI, dispatch the URI to the correct method +to handle it. Returns the results of the method. + +=head3 $uri + +A uri, of the form accepted by URI. + +=cut + +sub fetch { + my ($self, $uri ) = @_; + my $guts = {}; + my $urio = URI->new($uri); + my $scheme = $urio->scheme; + if ($scheme eq 'http' or $scheme eq 'https') { + $guts = $self->fetchHttp($urio); + } + elsif ($scheme eq 'asset') { + $guts = $self->fetchAsset($urio); + } + elsif ($scheme eq 'file') { + $guts = $self->fetchFile($urio); + } + return $guts; +} + +#------------------------------------------------------------------- + +=head2 fetchAsset ( $uri ) + +Fetches a bundle file from a WebGUI Asset (probably a snippet) in this site. +If the Asset cannot be found with that URL, it returns an empty hashref. +Depending on the type of Asset fetched, there will be different fields. Every +kind of asset will have the lastModified field. + +Snippet assets will have a content field with the contents of the Snippet inside +of it. + +File assets will have a content field with the contents of the file. + +Any other kind of asset will return an empty content field. + +=head3 $uri + +A URI object. + +=cut + +sub fetchAsset { + my ($self, $uri ) = @_; + + my $url = $uri->opaque; + $url =~ s{^/+}{}; + my $asset = WebGUI::Asset->newByUrl($self->session, $url); + return {} unless $asset; + ##Check for a snippet, or snippet subclass? + my $guts = { + lastModified => $asset->get('lastModified'), + content => '', + }; + if ($asset->isa('WebGUI::Asset::Snippet')) { + $guts->{content} = $asset->view(1); + } + elsif ($asset->isa('WebGUI::Asset::File')) { + $guts->{content} = $asset->getStorageLocation->getFileContentsAsScalar($asset->get('filename')); + } + return $guts; +} + +#------------------------------------------------------------------- + +=head2 fetchFile ( $uri ) + +Fetches a bundle file from the local filesystem. Returns a hashref +with the content and date that it was last updated. If there is any problem +with getting the file, it returns an empty hashref. + +=head3 $uri + +A URI object. + +=cut + +sub fetchFile { + my ($self, $uri ) = @_; + my $filepath = $uri->path; + return {} unless (-e $filepath && -r _); + my @stats = stat(_); # recycle stat data from file tests. + open my $file, '<', $filepath or return {}; + local $/; + my $guts = { + lastModified => $stats[9], + content => <$file>, + }; + close $file; + return $guts; +} + +#------------------------------------------------------------------- + +=head2 fetchHttp ( $uri ) + +Fetches a bundle file from the web. Returns a hashref with the content +and date that it was last updated. If there is any problem with making +the request, it returns an empty hashref. + +=head3 $uri + +A URI object. + +=cut + +sub fetchHttp { + my ($self, $uri ) = @_; + + # Set up LWP + my $userAgent = LWP::UserAgent->new; + $userAgent->env_proxy; + $userAgent->agent("WebGUI"); + + # Create a request and stuff the uri in it + my $request = HTTP::Request->new( GET => $uri ); + my $response = $userAgent->request($request); + + if (! $response->is_success) { + return {}; + } + my $guts = { + content => $response->content, + lastModified => $response->header('last-modified'), + }; + return $guts; +} + +#------------------------------------------------------------------- + +=head2 bundleUrl ( ) + +Returns a urlized version of the bundle name, safe for URLs and filenames. + +=cut + +sub bundleUrl { + my ($self) = @_; + return $self->session->url->urlize($self->get('bundleName')); +} + +#------------------------------------------------------------------- + +=head2 getCollateral ( tableName, keyName, keyValue ) + +Returns a hash reference containing one row of collateral data from a particular +table. + +=head3 tableName + +The name of the table you wish to retrieve the data from. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to get. +If this is equal to "new", then an empty hashRef will be returned to avoid +strict errors in the caller. If the requested data does not exist in the +collateral array, it also returns an empty hashRef. + +=cut + +sub getCollateral { + my $self = shift; + my $tableName = shift; + my $keyName = shift; + my $keyValue = shift; + if ($keyValue eq "new" || $keyValue eq "") { + return {}; + } + my $table = $self->get($tableName); + my $index = $self->getCollateralDataIndex($table, $keyName, $keyValue); + return {} if $index == -1; + my %copy = %{ $table->[$index] }; + return \%copy; +} + + +#------------------------------------------------------------------- + +=head2 getCollateralDataIndex ( table, keyName, keyValue ) + +Returns the index in a set of collateral where an element of the +data (keyName) has a certain value (keyValue). If the criteria +are not found, returns -1. + +=head3 table + +The collateral data to search + +=head3 keyName + +The name of a key in the collateral hash. + +=head3 keyValue + +The value that keyName should have to meet the criteria. + +=cut + +sub getCollateralDataIndex { + my $self = shift; + my $table = shift; + my $keyName = shift; + my $keyValue = shift; + for (my $index=0; $index <= $#{ $table }; $index++) { + return $index + if (exists($table->[$index]->{$keyName}) && ($table->[$index]->{$keyName} eq $keyValue )); + } + return -1; +} + +#------------------------------------------------------------------- + +=head2 getPathClassDir ( $otherBuild ) + +Returns a Path::Class::Dir object to the last build directory +for this bundle. + +=head3 $otherBuild + +Another time stamp to use instead of the lastModified timestamp. + +=cut + +sub getPathClassDir { + my ($self, $lastBuild) = @_; + $lastBuild ||= $self->get('lastBuild'); + return Path::Class::Dir->new( + $self->session->config->get('uploadsPath'), + 'filepump', + $self->bundleUrl . '.' . $lastBuild + ); +} + +#------------------------------------------------------------------- + +=head2 getOutOfDateBundles ( $session ) + +This is a class method. It returns an array reference of WebGUI::FilePump::Bundle +objects that need to be rebuilt. + +=head3 $session + +A WebGUI::Session object. + +=cut + +sub getOutOfDateBundles { + my ($class, $session) = @_; + my $oldBundles = []; + my $oldBundleIterator = $class->getAllIterator({ + constraints => [ + 'lastBuild < lastModified' => [], + ], + }); + while (my $bundle = $oldBundleIterator->()) { + push @{ $oldBundles }, $bundle; + } + return $oldBundles; +} + +#------------------------------------------------------------------- + +=head2 moveCollateralDown ( tableName, keyName, keyValue ) + +Moves a collateral data item down one position. If called on the last element of the +collateral array then it does nothing. Returns 1 if the move is successful. Returns +undef or the empty array otherwise. + +=head3 tableName + +A string indicating the table that contains the collateral data. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to move. + +=cut + +sub moveCollateralDown { + my $self = shift; + my $tableName = shift; + my $keyName = shift; + my $keyValue = shift; + + my $table = $self->get($tableName); + my $index = $self->getCollateralDataIndex($table, $keyName, $keyValue); + return if $index == -1; + return unless (abs($index) < $#{$table}); + @{ $table }[$index,$index+1] = @{ $table }[$index+1,$index]; + $self->update({ $tableName => $table }); + return 1; +} + + +#------------------------------------------------------------------- + +=head2 moveCollateralUp ( tableName, keyName, keyValue ) + +Moves a collateral data item up one position. If called on the first element of the +collateral array then it does nothing. Returns 1 if the move is successful. Returns +undef or the empty array otherwise. + + +=head3 tableName + +A string indicating the table that contains the collateral data. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to move. + +=cut + +sub moveCollateralUp { + my $self = shift; + my $tableName = shift; + my $keyName = shift; + my $keyValue = shift; + + my $table = $self->get($tableName); + my $index = $self->getCollateralDataIndex($table, $keyName, $keyValue); + return if $index == -1; + return unless $index && (abs($index) <= $#{$table}); + @{ $table }[$index-1,$index] = @{ $table }[$index,$index-1]; + $self->update({ $tableName => $table }); + return 1; +} + +#------------------------------------------------------------------- + +=head2 moveFileDown ( $type, $fileId ) + +Moves the requested file down in the ordered collateral. + +=head3 $type + +If $type is JS, it moves a file in the javascript part of the bundle. If it is +CSS, it moves a file in the CSS part of the bundle. OTHER is used for all other +types of files. + +=head3 $fileId + +The unique collateral GUID to move in the bundle. + +=cut + +sub moveFileDown { + my ($self, $type, $fileId) = @_; + return 0, 'Illegal type' unless WebGUI::Utility::isIn($type, 'JS', 'CSS', 'OTHER'); + return 0, 'No fileId' unless $fileId; + my $collateralType = $type eq 'JS' ? 'jsFiles' + : $type eq 'CSS' ? 'cssFiles' + : 'otherFiles'; + $self->moveCollateralDown( + $collateralType, + 'fileId', + $fileId, + ); + $self->update({lastModified => time()}); + return 1; +} + +#------------------------------------------------------------------- + +=head2 moveFileUp ( $type, $fileId ) + +Moves the requested file up in the ordered collateral. + +=head3 $type + +If $type is JS, it moves a file in the javascript part of the bundle. If it is +CSS, it moves a file in the CSS part of the bundle. OTHER is used for all other +types of files. + +=head3 $fileId + +The unique collateral GUID to move in the bundle. + +=cut + +sub moveFileUp { + my ($self, $type, $fileId) = @_; + return 0, 'Illegal type' unless WebGUI::Utility::isIn($type, 'JS', 'CSS', 'OTHER'); + return 0, 'No fileId' unless $fileId; + my $collateralType = $type eq 'JS' ? 'jsFiles' + : $type eq 'CSS' ? 'cssFiles' + : 'otherFiles'; + $self->moveCollateralUp( + $collateralType, + 'fileId', + $fileId, + ); + $self->update({lastModified => time()}); + return 1; +} + + +#----------------------------------------------------------------- + +=head2 setCollateral ( tableName, keyName, keyValue, properties ) + +Performs and insert/update of collateral data for any wobject's collateral data. +Returns the id of the data that was set, even if a new row was added to the +data. + +=head3 tableName + +The name of the table to insert the data. + +=head3 keyName + +The name of a key in the collateral hash. Typically a unique identifier for a given +"row" of collateral data. + +=head3 keyValue + +Along with keyName, determines which "row" of collateral data to set. +The index of the collateral data to set. If the keyValue = "new", then a +new entry will be appended to the end of the collateral array. Otherwise, +the appropriate entry will be overwritten with the new data. + +=head3 properties + +A hash reference containing the name/value pairs to be inserted into the collateral, using +the criteria mentioned above. + +=cut + +sub setCollateral { + my $self = shift; + my $tableName = shift; + my $keyName = shift; + my $keyValue = shift; + my $properties = shift; + ##Note, since this returns a reference, it is actually updating + ##the object cache directly. + my $table = $self->get($tableName); + if ($keyValue eq 'new' || $keyValue eq '') { + if (! exists $properties->{$keyName} + or $properties->{$keyName} eq 'new' + or $properties->{$keyName} eq '') { + $properties->{$keyName} = $self->session->id->generate; + } + push @{ $table }, $properties; + $self->update({$tableName => $table}); + return $properties->{$keyName}; + } + my $index = $self->getCollateralDataIndex($table, $keyName, $keyValue); + return if $index == -1; + $table->[$index] = $properties; + $self->update({ $tableName => $table }); + return $keyValue; +} + + +1; diff --git a/lib/WebGUI/Macro/FilePump.pm b/lib/WebGUI/Macro/FilePump.pm new file mode 100644 index 000000000..6e3fc584c --- /dev/null +++ b/lib/WebGUI/Macro/FilePump.pm @@ -0,0 +1,103 @@ +package WebGUI::Macro::FilePump; + +#------------------------------------------------------------------- +# WebGUI is Copyright 2001-2009 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 +#------------------------------------------------------------------- + +use strict; +use WebGUI::FilePump::Bundle; + +=head1 NAME + +Package WebGUI::Macro::Build + +=head1 DESCRIPTION + +Macro to access FilePump bundle information. + +=head2 process( $session, $bundleName, $type ) + +Deliver the bundle files. If in admin mode, give raw links to the files. +If not in admin mode, give links to the bundled, minified files. + +=over 4 + +=item * + +A session variable + +=item * + +$bundleName, the name of a File Pump bundle. + +=item * + +$type, the type of files from the Bundle that you are accessing. Either JS or javascript, or CSS or css. + +=back + +=cut + + +#------------------------------------------------------------------- +sub process { + my $session = shift; + my $bundleName = shift; + my $type = shift; + $type = lc $type; + my $output = ""; + + my $bundleId = WebGUI::FilePump::Bundle->getAllIds($session, { + constraints => [ { 'bundleName = ?' => [$bundleName]}, ], + limit => 1, + }); + return '' unless $bundleId and $bundleId->[0]; + + my $bundle = WebGUI::FilePump::Bundle->new($session, $bundleId); + return '' unless $bundle; + + if (! $session->var->isAdminOn) { + my $dir = $bundle->getPathClassDir; + if ($type eq 'js' || $type eq 'javascript') { + my $file = $dir->file($bundle->bundleUrl . '.js'); + return sprintf qq|