diff --git a/docs/changelog/7.x.x.txt b/docs/changelog/7.x.x.txt index b207c5081..0b3b2e21a 100644 --- a/docs/changelog/7.x.x.txt +++ b/docs/changelog/7.x.x.txt @@ -13,6 +13,9 @@ - Graphics::Magick is now the standard graphics package in WebGUI, but Image::Magick will be supported for backwards compatibility. See gotcha.txt for details. + - fix: A bug where it was possible to delete a system page if it were made + the child of a non-system page that you had edit rights to. + - api: Added a unified contraints system for the file and image assets. - Added file attachments to the Wiki. - Added a new attachments form control. - Added a form control skeleton. diff --git a/docs/upgrades/upgrade_7.3.22-7.4.0.pl b/docs/upgrades/upgrade_7.3.22-7.4.0.pl index 65e4daa01..54f7f1d19 100644 --- a/docs/upgrades/upgrade_7.3.22-7.4.0.pl +++ b/docs/upgrades/upgrade_7.3.22-7.4.0.pl @@ -98,6 +98,7 @@ sub addWikiAttachments { $db->write("alter table WikiPage drop column storageId"); my $root = WebGUI::Asset->getRoot($session); $root->addChild({title=>"Tempspace", url=>"tempspace", className=>"WebGUI::Asset::Wobject::Folder"}, "tempspace0000000000000"); + $db->write("update asset set isSystem=1 where assetId=?", ["tempspace0000000000000"]); } #------------------------------------------------- diff --git a/lib/WebGUI/Asset/File.pm b/lib/WebGUI/Asset/File.pm index 50571acb0..da096ae3b 100644 --- a/lib/WebGUI/Asset/File.pm +++ b/lib/WebGUI/Asset/File.pm @@ -67,6 +67,25 @@ sub addRevision { return $newSelf; } +#------------------------------------------------------------------- + +=head2 applyConstraints ( options ) + +Enforce certain things when new files are uploaded. + +=head3 options + +A hash reference of optional parameters. None at this time. + +=cut + +sub applyConstraints { + my $self = shift; + $self->getStorageLocation->setPrivileges($self->get('ownerUserId'), $self->get('groupIdView'), $self->get('groupIdEdit')); + $self->setSize; +} + + #------------------------------------------------------------------- =head2 definition ( definition ) @@ -285,7 +304,6 @@ sub processPropertiesFromFormPost { my $storage = $self->getStorageFromPost($storageId); if (defined $storage) { - $storage->setPrivileges($self->get('ownerUserId'), $self->get('groupIdView'), $self->get('groupIdEdit')); my $filename = $storage->getFiles()->[0]; if (defined $filename) { @@ -299,6 +317,7 @@ sub processPropertiesFromFormPost { $self->update(\%data); } } + $self->applyConstraints; } diff --git a/lib/WebGUI/Asset/File/Image.pm b/lib/WebGUI/Asset/File/Image.pm index 58e7a9199..ffa6cb19e 100644 --- a/lib/WebGUI/Asset/File/Image.pm +++ b/lib/WebGUI/Asset/File/Image.pm @@ -46,6 +46,53 @@ These methods are available from this class: +#------------------------------------------------------------------- + +=head2 applyConstraints ( options ) + +Things that are done after a new file is attached. + +=head3 options + +A hash reference of optional parameters. + +=head4 maxImageSize + +An integer (in pixels) representing the longest edge the image may have. + +=head4 thumbnailSize + +An integer (in pixels) representing the longest edge a thumbnail may have. + +=cut + +sub applyConstraints { + my $self = shift; + my $options = shift; + $self->SUPER::applyConstraints($options); + my $maxImageSize = $options->{maxImageSize} || $self->session->setting->get("maxImageSize"); + my $thumbnailSize = $options->{thumbnailSize} || $self->session->setting->get("thumbnailSize"); + my $parameters = $self->get("parameters"); + my $storage = $self->getStorageLocation; + unless ($parameters =~ /alt\=/) { + $self->update({parameters=>$parameters.' alt="'.$self->get("title").'"'}); + } + my $file = $self->get("filename"); + my ($w, $h) = $storage->getSizeInPixels($file); + if($w > $maxImageSize || $h > $maxImageSize) { + if($w > $h) { + $storage->resize($file, $maxImageSize); + } + else { + $storage->resize($file, 0, $maxImageSize); + } + } + $self->generateThumbnail($thumbnailSize); + $self->setSize; +} + + + #------------------------------------------------------------------- =head2 definition ( definition ) @@ -219,23 +266,7 @@ sub prepareView { sub processPropertiesFromFormPost { my $self = shift; $self->SUPER::processPropertiesFromFormPost; - my $parameters = $self->get("parameters"); - my $storage = $self->getStorageLocation; - unless ($parameters =~ /alt\=/) { - $self->update({parameters=>$parameters.' alt="'.$self->get("title").'"'}); - } - my $max_size = $self->session->setting->get("maxImageSize"); - my $file = $self->get("filename"); - my ($w, $h) = $storage->getSizeInPixels($file); - if($w > $max_size || $h > $max_size) { - if($w > $h) { - $storage->resize($file, $max_size); - } - else { - $storage->resize($file, 0, $max_size); - } - } - $self->generateThumbnail($self->session->form->process("thumbnailSize")); + $self->applyConstraints; } #------------------------------------------------------------------- diff --git a/lib/WebGUI/Asset/FilePile.pm b/lib/WebGUI/Asset/FilePile.pm index d04e8ab78..753cf1303 100644 --- a/lib/WebGUI/Asset/FilePile.pm +++ b/lib/WebGUI/Asset/FilePile.pm @@ -165,20 +165,6 @@ sub editSave { $data{templateId} = 'PBtmpl0000000000000024'; if ($selfName eq "WebGUI::Asset::File::Image") { $data{templateId} = 'PBtmpl0000000000000088'; - $data{parameters} = 'alt="'.$self->get("title").'"'; - - # Resize image if it is bigger than the max allowed image size. - my $maxSize = $self->session->setting->get("maxImageSize"); - my ($width, $height) = $tempStorage->getSizeInPixels($filename); - if($width > $maxSize || $height > $maxSize) { - if($width > $height) { - $tempStorage->resize($filename, $maxSize); - } - else { - $tempStorage->resize($filename, 0, $maxSize); - } - } - } $data{url} = $self->getParent->get('url').'/'.$filename; @@ -188,11 +174,7 @@ sub editSave { #Get the current storage location my $storage = $newAsset->getStorageLocation(); $storage->addFileFromFilesystem($tempStorage->getPath($filename)); - $storage->setPrivileges($data{"ownerUserId"},$data{"groupIdView"},$data{"groupIdEdit"}); - - $newAsset->setSize($tempStorage->getFileSize($filename)); - $newAsset->generateThumbnail if ($selfName eq "WebGUI::Asset::File::Image"); - $newAsset->update({ storageId=> $storage->getId }); + $newAsset->applyConstraints; #Now remove the reference to the storeage location to prevent problems with different revisions. delete $newAsset->{_storageLocation}; diff --git a/lib/WebGUI/Asset/WikiPage.pm b/lib/WebGUI/Asset/WikiPage.pm index af21dee28..ab51fd0fe 100644 --- a/lib/WebGUI/Asset/WikiPage.pm +++ b/lib/WebGUI/Asset/WikiPage.pm @@ -175,7 +175,9 @@ sub getEditForm { } $var->{formAttachment} = WebGUI::Form::Attachments($session, { value => $children, - maxAttachments => $wiki->get("allowAttachments") + maxAttachments => $wiki->get("allowAttachments"), + maxImageSize => $wiki->get("maxImageSize"), + thumbnailSize => $wiki->get("thumbnailSize"), }); return $self->processTemplate($var, $wiki->getValue('pageEditTemplateId')); } @@ -236,6 +238,7 @@ sub processPropertiesFromFormPost { $self->update({isProtected => $self->session->form("isProtected")}); } + # deal with attachments from the attachments form control my @attachments = $self->session->form->param("attachments"); my @tags = (); foreach my $assetId (@attachments) { @@ -243,11 +246,18 @@ sub processPropertiesFromFormPost { if (defined $asset) { unless ($asset->get("parentId") eq $self->getId) { $asset->setParent($self); + $asset->update({ + ownerUserId => $self->get("ownerUserId"), + groupIdEdit => $self->get("groupIdEdit"), + groupIdView => $self->get("groupIdView"), + }); } push(@tags, $asset->get("tagId")); $asset->setVersionTag($self->get("tagId")); } } + + # clean up empty tags foreach my $tag (@tags) { my $version = WebGUI::VersionTag->new($self->session, $tag); if (defined $version) { @@ -256,6 +266,8 @@ sub processPropertiesFromFormPost { } } } + + # wiki pages are auto committed $self->requestAutoCommit; } diff --git a/lib/WebGUI/AssetLineage.pm b/lib/WebGUI/AssetLineage.pm index c440c3c8d..2cfe6e857 100644 --- a/lib/WebGUI/AssetLineage.pm +++ b/lib/WebGUI/AssetLineage.pm @@ -260,11 +260,12 @@ A hash reference comprising modifiers to relative listing. Rules include: =head4 statesToInclude -An array reference containing a list of states that should be returned. Defaults to 'published'. Options include 'published', 'trash', 'cliboard', 'clipboard-limbo' and 'trash-limbo'. +An array reference containing a list of states that should be returned. Defaults to 'published'. Options include +'published', 'trash', 'clipboard', 'clipboard-limbo' and 'trash-limbo'. =head4 statusToInclude -An array reference containing a list of status that should be returned. Defaults to 'approved'. Options include 'approved', 'pending', 'deleted', and 'archived'. +An array reference containing a list of status that should be returned. Defaults to 'approved'. Options include 'approved', 'pending', and 'archived'. =head4 endingLineageLength diff --git a/lib/WebGUI/AssetTrash.pm b/lib/WebGUI/AssetTrash.pm index 88e015503..6a963a0b9 100644 --- a/lib/WebGUI/AssetTrash.pm +++ b/lib/WebGUI/AssetTrash.pm @@ -95,7 +95,8 @@ sub getAssetsInTrash { =head2 purge ( [ options ] ) -Deletes an asset from tables and removes anything bound to that asset, including descendants. +Deletes an asset from tables and removes anything bound to that asset, including descendants. Returns 1 on success +and 0 on failure. =head3 options @@ -110,30 +111,66 @@ A boolean that, if true, will skip dealing with exported files. sub purge { my $self = shift; my $options = shift; - return undef if ($self->getId eq $self->session->setting->get("defaultPage") || $self->getId eq $self->session->setting->get("notFoundPage") || $self->get("isSystem")); + my $session = $self->session; + + # can't delete if it's one of these things + if ($self->getId eq $session->setting->get("defaultPage") || $self->getId eq $session->setting->get("notFoundPage") || $self->get("isSystem")) { + $session->errorHandler->security("delete a system protected page (".$self->getId.")"); + return 0; + } + + # assassinate the offspring + my $kids = $self->getLineage(["children"],{returnObjects=>1, statesToInclude=>['published', 'clipboard', 'clipboard-limbo','trash','trash-limbo']}); + foreach my $kid (@{$kids}) { + # Technically get lineage should never return an undefined object from getLineage when called like this, but it did so this saves the world from destruction. + if (defined $kid) { + unless ($kid->purge) { + $self->errorHandler->security("delete one of (".$self->getId.")'s children which is a system protected page"); + return 0; + } + } + else { + $session->errorHandler->error("getLineage returned an undefined object in the AssetTrash->purge method. Unable to purge asset."); + } + } + + # gotta delete stuff we've exported unless ($options->{skipExported}) { $self->_invokeWorkflowOnExportedFiles($self->session->setting->get('purgeWorkflow'), 1); } - my $kids = $self->getLineage(["children"],{returnObjects=>1, statesToInclude=>['published', 'clipboard', 'clipboard-limbo','trash','trash-limbo']}); - foreach my $kid (@{$kids}) { - # Technically get lineage should never return an undefined object from getLineage when called like this, but it did so this saves the world from destruction. - (defined $kid) ? $kid->purge : - $self->session->errorHandler->warn("getLineage returned an undefined object in the AssetTrash->purge method. Unable to purge asset."); - } - WebGUI::Keyword->new($self->session)->deleteKeywordsForAsset($self); + # gonna need this at the end + my $tagId = $self->get("tagId"); + + # clean up keywords + WebGUI::Keyword->new($session)->deleteKeywordsForAsset($self); + + # clean up search engine WebGUI::Search::Index->new($self)->delete; - $self->session->db->beginTransaction; - $self->session->db->write("delete from metaData_values where assetId = ".$self->session->db->quote($self->getId)); - foreach my $definition (@{$self->definition($self->session)}) { - $self->session->db->write("delete from ".$definition->{tableName}." where assetId=".$self->session->db->quote($self->getId)); - } - $self->session->db->write("delete from asset where assetId=".$self->session->db->quote($self->getId)); - $self->session->db->commit; + + # clean up cache + WebGUI::Cache->new($session)->deleteChunk(["asset",$self->getId]); $self->purgeCache; - WebGUI::Cache->new($self->session)->deleteChunk(["asset",$self->getId]); + + # delete stuff out of the asset tables + $session->db->beginTransaction; + $session->db->write("delete from metaData_values where assetId = ?",[$self->getId]); + foreach my $definition (@{$self->definition($session)}) { + $session->db->write("delete from ".$definition->{tableName}." where assetId=?", [$self->getId]); + } + $session->db->write("delete from asset where assetId=?", [$self->getId]); + $session->db->commit; + + # log that we've purged this asset $self->updateHistory("purged"); $self = undef; + + # clean up version tag if empty + my $versionTag = WebGUI::VersionTag->new($session, $tagId); + if ($versionTag->getAssetCount == 0) { + $versionTag->rollback; + } + return 1; } diff --git a/lib/WebGUI/Form/Attachments.pm b/lib/WebGUI/Form/Attachments.pm index e485b2fe8..63e27f796 100644 --- a/lib/WebGUI/Form/Attachments.pm +++ b/lib/WebGUI/Form/Attachments.pm @@ -69,6 +69,16 @@ How many attachments will be allowed to be uploaded. Defaults to 1. An array reference of asset objects (not ids, but objects) that should be displayed in the attachments box. +=head4 maxImageSize + +An integer (in pixels) of the maximum height or width an image can be. Defaults to the size in the main settings if +not specified. + +=head4 thumbnailSize + +An integer (in pixels) of the proportional size of a thumbnail for an image. Defaults to the size in the main +settings if not specified. + =cut sub definition { @@ -86,6 +96,8 @@ sub definition { maxAttachments=>{ defaultValue=>1 }, + maxImageSize=>{}, + thumbnailSize=>{}, profileEnabled=>{ defaultValue=>0 }, @@ -122,11 +134,14 @@ Renders an attachments control. sub toHtml { my $self = shift; my @assetIds = @{$self->get("value")}; + my $thumbnail = $self->get("thumbnailSize") || $self->session->setting->get("thumbnailSize"); + my $image = $self->get("maxImageSize") || $self->session->setting->get("maxImageSize"); my $attachmentsList = "attachments=".join(";attachments=", @assetIds) if (scalar(@assetIds)); return '
'; + .";maxAttachments=".$self->get("maxAttachments")).";maxImageSize=".$image.";thumbnailSize=" + .$thumbnail.";".$attachmentsList + .'" style="width: 100%; height: 120px;">
'; } @@ -200,6 +215,8 @@ sub www_show { X
+ + @@ -215,7 +232,8 @@ sub www_show { my $asset = WebGUI::Asset->newByDynamicClass($session, $assetId); if (defined $asset) { $attachments .= '
param("maxAttachments").";" + .$url->page("op=formHelper;class=Attachments;sub=delete;maxAttachments=".$form->param("maxAttachments") + .";maxImageSize=".$form->param("maxImageSize").";thumbnailSize=".$form->param("thumbnailSize").";" .$attachmentsList.";assetId=".$assetId.";name=".$form->param("name")).'" class="deleteAttachment">X '; if ($asset->isa("WebGUI::Asset::File::Image")) { @@ -257,31 +275,31 @@ sub www_upload { my $storage = WebGUI::Storage::Image->createTemp($session); my $filename = $storage->addFileFromFormPost("attachment"); my $tempspace = WebGUI::Asset->getTempspace($session); + my $asset = ""; + + # prevent malicious visitors from being able to publish children things they've published to tempsace + my $owner = ($session->user->userId eq "1") ? "3" : $session->user->userId; + + my %properties = ( + title => $filename, + url => "attachments/".$filename, + filename => $filename, + ownerUserId => $owner, + groupIdEdit => "3", + groupIdView => "7", + ); if ($storage->isImage($filename)) { - my $image = $tempspace->addChild({ - title => $filename, - url => "attachments/".$filename, - className => "WebGUI::Asset::File::Image", - filename => $filename, - templateId => "PBtmpl0000000000000088", - }); - $image->getStorageLocation->addFileFromFilesystem($storage->getPath($filename)); - $image->generateThumbnail(); - $image->setSize; - push(@assetIds, $image->getId); + $properties{className} = "WebGUI::Asset::File::Image"; + $properties{templateId} = "PBtmpl0000000000000088"; } else { - my $file = $tempspace->addChild({ - title => $filename, - url => "attachments/".$filename, - className => "WebGUI::Asset::File", - filename => $filename, - templateId => "PBtmpl0000000000000024", - }); - $file->getStorageLocation->addFileFromFilesystem($storage->getPath($filename)); - $file->setSize; - push(@assetIds, $file->getId); + $properties{className} = "WebGUI::Asset::File"; + $properties{templateId} = "PBtmpl0000000000000024"; } + $asset = $tempspace->addChild(\%properties); + $asset->getStorageLocation->addFileFromFilesystem($storage->getPath($filename)); + $asset->applyConstraints; + push(@assetIds, $asset->getId); if ($session->setting->get("autoRequestCommit")) { WebGUI::VersionTag->getWorking($session)->requestCommit; } diff --git a/lib/WebGUI/Setup.pm b/lib/WebGUI/Setup.pm index f092fee33..65a9b33e1 100644 --- a/lib/WebGUI/Setup.pm +++ b/lib/WebGUI/Setup.pm @@ -478,7 +478,7 @@ a:visited { color: '.$form->get("visitedLinkColor").'; } # commit the working tag my $working = WebGUI::VersionTag->getWorking($session); - $working->set({title=>"Initial Site Setup"}); + $working->set({name=>"Initial Site Setup"}); $working->commit; # remove init state diff --git a/lib/WebGUI/Workflow/Activity/CleanTempStorage.pm b/lib/WebGUI/Workflow/Activity/CleanTempStorage.pm index 3db0186a7..9bae195b9 100644 --- a/lib/WebGUI/Workflow/Activity/CleanTempStorage.pm +++ b/lib/WebGUI/Workflow/Activity/CleanTempStorage.pm @@ -19,6 +19,7 @@ use strict; use base 'WebGUI::Workflow::Activity'; use File::Path; use File::stat; +use WebGUI::Asset; =head1 NAME @@ -102,7 +103,27 @@ See WebGUI::Workflow::Activity::execute() for details. sub execute { my $self = shift; - $self->recurseFileSystem($self->session->config->get("uploadsPath")."/temp"); + my $start = time(); + + # kill temporary assets + my $tempspace = WebGUI::Asset->getTempspace($self->session); + my $children = $tempspace->getLineage(["children"], { + returnObjects => 1, + statesToInclude => [qw(trash clipboard published)], + statusToInclude => [qw(pending archived approved)], + }); + foreach my $asset (@{$children}) { + if (time() - $asset->get("revisionDate") > $self->get("storageTimeout")) { + unless ($asset->purge) { + return $self->ERROR; + } + } + # taking too long, give up + return $self->WAITING if (time() - $start > 50); + } + + # kill temporary files + return $self->recurseFileSystem($start, $self->session->config->get("uploadsPath")."/temp"); } @@ -120,20 +141,27 @@ The starting path. sub recurseFileSystem { my $self = shift; + my $start = shift; my $path = shift; - my (@filelist, $file); - if (opendir(DIR,$path)) { - @filelist = readdir(DIR); - closedir(DIR); - foreach $file (@filelist) { - unless ($file eq "." || $file eq "..") { - $self->recurseFileSystem($path."/".$file); - if ($self->checkFileAge($path."/".$file)) { - rmtree($path."/".$file); - } - } + my (@filelist, $file); + if (opendir(DIR,$path)) { + @filelist = readdir(DIR); + closedir(DIR); + foreach $file (@filelist) { + unless ($file eq "." || $file eq "..") { + # taking too long, time to abort + return $self->WAITING if (time() - $start > 50); + + # must search for children + $self->recurseFileSystem($start, $path."/".$file); + + # if it's old enough, let's kill it + if ($self->checkFileAge($path."/".$file)) { + rmtree($path."/".$file); } + } } + } return $self->COMPLETE; }