From b465a46bcc7824fbb0548c31a04de0db0c0ee4c8 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Sun, 6 Apr 2008 08:20:13 +0000 Subject: [PATCH] API CHANGErefactored upload functionality from File into newly created Upload class API CHANGE deprecated some File functions and attributes API CHANGE moved management function from File to Filesystem and added permission checks: sync(), loadContent(), fixfiles(), moverootfilesto() API CHANGE deprecated use of File->loadUploaded() ENHANCEMENT added filesize and extension validation to AssetAdmin and FileField FEATURE added tests for Upload class Merged revisions 47617 via svnmerge from svn://svn.silverstripe.com/silverstripe/modules/cms/branches/2.2.0-mesq ........ r47617 | ischommer | 2008-01-04 19:20:29 +1300 (Fri, 04 Jan 2008) | 5 lines git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@52205 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- filesystem/File.php | 349 ++++++++++++-------------------- filesystem/Filesystem.php | 62 +++++- filesystem/Upload.php | 325 +++++++++++++++++++++++++++++ forms/EditableFileField.php | 45 ++-- forms/FileField.php | 199 ++++++++++++++++-- tests/filesystem/UploadTest.php | 63 ++++++ 6 files changed, 770 insertions(+), 273 deletions(-) create mode 100644 filesystem/Upload.php create mode 100644 tests/filesystem/UploadTest.php diff --git a/filesystem/File.php b/filesystem/File.php index 6998fbc13..efbf3aa00 100755 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -15,19 +15,9 @@ * @subpackage filesystem */ class File extends DataObject { + static $default_sort = "Name"; - /** - * @var array Key is the extension, which has an array of MaxSize and WarnSize, - * e.g. array("jpg" => array("MaxSize"=>1000, "WarnSize=>500")) - */ - static $file_size_restrictions = array(); - - /** - * @var array Collection of extensions, e.g. array("jpg","gif") - */ - static $allowed_file_types = array(); - static $singular_name = "File"; static $plural_name = "Files"; @@ -39,16 +29,20 @@ class File extends DataObject { "Content" => "Text", "Sort" => "Int" ); + static $indexes = array( "SearchFields" => "fulltext (Filename,Title,Content)", ); + static $has_one = array( "Parent" => "File", "Owner" => "Member" ); + static $extensions = array( "Hierarchy", ); + static $belongs_many_many = array( "BackLinkTracking" => "SiteTree", ); @@ -60,32 +54,41 @@ class File extends DataObject { * @var array */ protected static $cache_file_fields = null; + + function Link($action = null) { + return Director::baseURL() . $this->RelativeLink($action); + } + function RelativeLink($action = null){ + return $this->Filename; + } + + function TreeTitle() { + return $this->Title; + } /** - * Set the maximum + * Event handler called before deleting from the database. + * You can overload this to clean up or otherwise process data before delete this + * record. Don't forget to call parent::onBeforeDelete(), though! */ - static function setMaxFileSize( $maxSize, $warningSize, $extension = '*' ) { - self::$file_size_restrictions[$extension]['MaxSize'] = $maxSize; - self::$file_size_restrictions[$extension]['WarnSize'] = $warningSize; - } - - static function getMaxFileSize($extension = '*') { - if(!isset(self::$file_size_restrictions[$extension])) { - if(isset(self::$file_size_restrictions['*'])) { - $extension = '*'; - } else { - return null; - } + protected function onBeforeDelete() { + parent::onBeforeDelete(); + + $this->autosetFilename(); + if($this->Filename && $this->Name && file_exists($this->getFullPath()) && !is_dir($this->getFullPath())) { + unlink($this->getFullPath()); } - return array( self::$file_size_restrictions[$extension]['MaxSize'], self::$file_size_restrictions[$extension]['WarnSize'] ); + if($brokenPages = $this->BackLinkTracking()) { + foreach($brokenPages as $brokenPage) { + Notifications::event("BrokenLink", $brokenPage, $brokenPage->OwnerID); + $brokenPage->HasBrokenFile = true; + $brokenPage->write(); + } + } } - - static function allowedFileType( $extension ) { - return true; - } - + /* * Find the given file */ @@ -138,19 +141,6 @@ class File extends DataObject { function Icon() { $ext = $this->Extension; if(!Director::fileExists("sapphire/images/app_icons/{$ext}_32.gif")) { - /*switch($ext) { - case "aif": case "au": case "mid": case "midi": case "mp3": case "ra": case "ram": case "rm": - case "mp3": case "wav": case "m4a": - $ext = "audio"; break; - - case "arc": case "rar": case "tar": case "gz": case "tgz": case "bz2": case "dmg": - $ext = "zip"; break; - - case "bmp": case "gif": case "jpg": case "jpeg": case "pcx": case "tif": case "png": - $ext = "image"; break; - - }*/ - $ext = $this->appCategory(); } @@ -162,81 +152,20 @@ class File extends DataObject { } /** - * Save an file passed from a form post into this object + * Save an file passed from a form post into this object. + * DEPRECATED Please instanciate an Upload-object instead and pass the file + * via {Upload->loadIntoFile()}. + * + * @param $tmpFile array Indexed array that PHP generated for every file it uploads. + * @return Boolean|string Either success or error-message. */ - function loadUploaded($tmpFile, $folderName = 'Uploads') { - if(!is_array($tmpFile)) user_error("File::loadUploaded() Not passed an array. Most likely, the form hasn't got the right enctype", E_USER_ERROR); - if(!$tmpFile['size']) return; + function loadUploaded($tmpFile) { + user_error('File::loadUploaded is deprecated, please use the Upload class directly.', E_USER_NOTICE); + $upload = new Upload(); + $upload->loadIntoFile($tmpFile, $this); - // @TODO This puts a HUGE limitation on files especially when lots - // have been uploaded. - $base = dirname(dirname($_SERVER['SCRIPT_FILENAME'])); - $class = $this->class; - $parentFolder = Folder::findOrMake($folderName); - - // Create a folder for uploading. - if(!file_exists("$base/assets")){ - mkdir("$base/assets", Filesystem::$folder_create_mask); - } - if(!file_exists("$base/assets/$folderName")){ - mkdir("$base/assets/$folderName", Filesystem::$folder_create_mask); - } - - // Generate default filename - $file = str_replace(' ', '-',$tmpFile['name']); - $file = ereg_replace('[^A-Za-z0-9+.-]+','',$file); - $file = ereg_replace('-+', '-',$file); - $file = basename($file); - - $file = "assets/$folderName/$file"; - - while(file_exists("$base/$file")) { - $i = isset($i) ? ($i+1) : 2; - $oldFile = $file; - if(substr($file, strlen($file) - strlen('.tar.gz')) == '.tar.gz' || - substr($file, strlen($file) - strlen('.tar.bz2')) == '.tar.bz2') { - $file = ereg_replace('[0-9]*(\.tar\.[^.]+$)',$i . '\\1', $file); - } else { - $file = ereg_replace('[0-9]*(\.[^.]+$)',$i . '\\1', $file); - } - if($oldFile == $file && $i > 2) user_error("Couldn't fix $file with $i", E_USER_ERROR); - } - - if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], "$base/$file")) { - // Update with the new image - /*$this->Filename = */ // $this->Name = null; - // $this->Filename = $file; - - // This is to prevent it from trying to rename the file - $this->record['Name'] = null; - $this->ParentID = $parentFolder->ID; - $this->Name = basename($file); - $this->write(); - return true; - } else { - user_error("File::loadUploaded: Couldn't copy '$tmpFile[tmp_name]' to '$file'", E_USER_ERROR); - return false; - } - } - - /** - * This function ensures the file table is correct with the files in the assets folder. - */ - static function sync() { - singleton('Folder')->syncChildren(); - $finished = false; - while(!$finished) { - $orphans = DB::query("SELECT C.ID FROM File AS C LEFT JOIN File AS P ON C.ParentID = P.ID WHERE P.ID IS NULL AND C.ParentID > 0"); - $finished = true; - if($orphans) foreach($orphans as $orphan) { - $finished = false; - // Delete the database record but leave the filesystem alone - $file = DataObject::get_by_id("File", $orphan['ID']); - $file->deleteDatabaseOnly(); - } - } - + return $upload->isError(); } /* @@ -246,79 +175,6 @@ class File extends DataObject { Debug::show(get_defined_functions()); } - function loadallcontent() { - ini_set("max_execution_time", 50000); - $allFiles = DataObject::get("File"); - $total = $allFiles->TotalItems(); - - $i = 0; - foreach($allFiles as $file) { - $i++; - $tmp = TEMP_FOLDER; - `echo "$i / $total" > $tmp/progress`; - $file->write(); - } - } - - /** - * Gets the content of this file and puts it in the field Content - */ - function loadContent() { - $filename = escapeshellarg($this->getFullPath()); - switch(strtolower($this->getExtension())){ - case 'pdf': - $content = `pdftotext $filename -`; - - //echo("
Content for $this->Filename:\n$content
"); - $this->Content = $content; - break; - case 'doc': - $content = `catdoc $filename`; - $this->Content = $content; - break; - case 'ppt': - $content = `catppt $filename`; - $this->Content = $content; - break; - case 'txt'; - $content = file_get_contents($this->FileName); - $this->Content = $content; - } - } - - function Link($action = null) { - return Director::baseURL() . $this->RelativeLink($action); - } - - function RelativeLink($action = null){ - return $this->Filename; - } - - function TreeTitle() { - if($this->hasMethod('alternateTreeTitle')) return $this->alternateTreeTitle(); - else return $this->Title; - } - - /** - * Event handler called before deleting from the database. - * You can overload this to clean up or otherwise process data before delete this - * record. Don't forget to call parent::onBeforeDelete(), though! - */ - protected function onBeforeDelete() { - parent::onBeforeDelete(); - - $this->autosetFilename(); - if($this->Filename && $this->Name && file_exists($this->getFullPath()) && !is_dir($this->getFullPath())) unlink($this->getFullPath()); - - if($brokenPages = $this->BackLinkTracking()) { - foreach($brokenPages as $brokenPage) { - Notifications::event("BrokenLink", $brokenPage, $brokenPage->OwnerID); - $brokenPage->HasBrokenFile = true; - $brokenPage->write(); - } - } - } - /** * Delete the database record (recursively for folders) without touching the filesystem */ @@ -343,8 +199,6 @@ class File extends DataObject { $brokenPage->write(); } } - - $this->loadContent(); } /** @@ -549,6 +403,7 @@ class File extends DataObject { function getExtension() { return strtolower(substr($this->getField('Filename'),strrpos($this->getField('Filename'),'.')+1)); } + function getFileType() { $types = array( 'gif' => 'GIF Image - good for diagrams', @@ -563,6 +418,7 @@ class File extends DataObject { 'pdf' => 'Adobe Acrobat PDF file', ); $ext = $this->getExtension(); + return isset($types[$ext]) ? $types[$ext] : 'unknown'; } @@ -571,13 +427,16 @@ class File extends DataObject { */ function getSize() { $size = $this->getAbsoluteSize(); - if($size){ - if($size < 1024) return $size . ' bytes'; - if($size < 1024*10) return (round($size/1024*10)/10). ' KB'; - if($size < 1024*1024) return round($size/1024) . ' KB'; - if($size < 1024*1024*10) return (round(($size/1024)/1024*10)/10) . ' MB'; - if($size < 1024*1024*1024) return round(($size/1024)/1024) . ' MB'; - } + + return ($size) ? self::format_size($size) : false; + } + + public static function format_size($size) { + if($size < 1024) return $size . ' bytes'; + if($size < 1024*10) return (round($size/1024*10)/10). ' KB'; + if($size < 1024*1024) return round($size/1024) . ' KB'; + if($size < 1024*1024*10) return (round(($size/1024)/1024*10)/10) . ' MB'; + if($size < 1024*1024*1024) return round(($size/1024)/1024) . ' MB'; } /** @@ -592,33 +451,6 @@ class File extends DataObject { } } - //--------------------------------------------------------------------------------------------------// - // Helper control functions - function moverootfilesto() { - if($folder = $this->urlParams[ID]) { - $newParent = Folder::findOrMake($folder); - $files = DataObject::get("File", "ClassName != 'Folder' AND ParentID = 0"); - foreach($files as $file) { - echo "
  • " , $file->RelativePath; - $file->ParentID = $newParent->ID; - echo " -> " , $file->RelativePath; - } - } - } - - /** - * Cleanup function to reset all the Filename fields. Visit File/fixfiles to call. - */ - function fixfiles() { - $files = DataObject::get("File"); - foreach($files as $file) { - $file->resetFilename(); - echo "
  • ", $file->Filename; - $file->write(); - } - echo "

    Done!"; - } - /** * Select clause for DataObject::get('File') operations/ @@ -676,7 +508,74 @@ class File extends DataObject { self::$cache_file_fields = null; } + + + + + + + + + + + /** + * DEPRECATED + */ + static $file_size_restrictions = array(); + + /** + * DEPRECATED + */ + static $allowed_file_types = array(); + + /** + * DEPRECATED + */ + static function setMaxFileSize( $maxSize, $warningSize, $extension = '*' ) { + user_error('File::setMaxFileSize is deprecated',E_USER_ERROR); + } + + /** + * DEPRECATED + */ + static function getMaxFileSize($extension = '*') { + user_error('File::getMaxFileSize is deprecated',E_USER_ERROR); + } + + /** + * DEPRECATED + */ + static function sync() { + user_error('File::sync is deprecated - please use FileSystem::sync',E_USER_ERROR); + } + + /** + * DEPRECATED + */ + function moverootfilesto() { + user_error('File::moverootfilesto is deprecated - please use FileSystem::moverootfilesto',E_USER_ERROR); + } + + /** + * DEPRECATED + */ + function fixfiles() { + user_error('File::fixfiles is deprecated - please use FileSystem::fixfiles',E_USER_ERROR); + } + +/** + * DEPRECATED + */ + function loadContent() { + user_error('File::loadContent deprecated',E_USER_ERROR); + } + + /** + * DEPRECATED + */ + public function loadallcontent() { + user_error('File::loadallcontent deprecated',E_USER_ERROR); + } } - ?> diff --git a/filesystem/Filesystem.php b/filesystem/Filesystem.php index b875dc7f9..4263489dd 100755 --- a/filesystem/Filesystem.php +++ b/filesystem/Filesystem.php @@ -1,12 +1,7 @@ urlParams['ID']) { + $newParent = Folder::findOrMake($folder); + $files = DataObject::get("File", "ClassName != 'Folder' AND ParentID = 0"); + foreach($files as $file) { + echo "

  • " , $file->RelativePath; + $file->ParentID = $newParent->ID; + echo " -> " , $file->RelativePath; + } + } + } + + /** + * Cleanup function to reset all the Filename fields. Visit File/fixfiles to call. + */ + public function fixfiles() { + if(!Permission::check('ADMIN')) Security::permissionFailure($this); + + $files = DataObject::get("File"); + foreach($files as $file) { + $file->resetFilename(); + echo "
  • ", $file->Filename; + $file->write(); + } + echo "

    Done!"; + } + /* * Return the most recent modification time of anything in the folder. * @param $folder The folder, relative to the site root * @param $extensionList An option array of file extensions to limit the search to */ - - protected static $cache_folderModTime; static function folderModTime($folder, $extensionList = null, $recursiveCall = false) { //$cacheID = $folder . ',' . implode(',', $extensionList); //if(!$recursiveCall && self::$cache_folderModTime[$cacheID]) return self::$cache_folderModTime[$cacheID]; @@ -89,7 +113,25 @@ class Filesystem extends Object { else return $filename[0] == '/'; } + /** + * This function ensures the file table is correct with the files in the assets folder. + */ + static function sync() { + singleton('Folder')->syncChildren(); + $finished = false; + while(!$finished) { + $orphans = DB::query("SELECT C.ID FROM File AS C LEFT JOIN File AS P ON C.ParentID = P.ID WHERE P.ID IS NULL AND C.ParentID > 0"); + $finished = true; + if($orphans) foreach($orphans as $orphan) { + $finished = false; + // Delete the database record but leave the filesystem alone + $file = DataObject::get_by_id("File", $orphan['ID']); + $file->deleteDatabaseOnly(); + } + } + } + } ?> \ No newline at end of file diff --git a/filesystem/Upload.php b/filesystem/Upload.php new file mode 100644 index 000000000..b45643a0f --- /dev/null +++ b/filesystem/Upload.php @@ -0,0 +1,325 @@ + + * array("jpg","GIF") + * + * + * @var array + */ + public $allowedExtensions = array(); + + /** + * Processing errors that can be evaluated, + * e.g. by Form-validation. + * + * @var array + */ + protected $errors = array(); + + /** + * A foldername relative to /assets, + * where all uploaded files are stored by default. + * + * @var string + */ + public static $uploads_folder = "Uploads"; + + /** + * Save an file passed from a form post into this object. + * + * @param $tmpFile array Indexed array that PHP generated for every file it uploads. + * @param $folderPath string Folder path relative to /assets + * @return Boolean|string Either success or error-message. + */ + function load($tmpFile, $folderPath = false) { + $this->clearErrors(); + + if(!$folderPath) $folderPath = self::$uploads_folder; + + if(!$this->file) $this->file = new File(); + + if(!is_array($tmpFile)) { + user_error("File::loadUploaded() Not passed an array. Most likely, the form hasn't got the right enctype", E_USER_ERROR); + } + + if(!$tmpFile['size']) { + $this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.'); + return false; + } + + $valid = $this->validate($tmpFile); + if(!$valid) return false; + + // @TODO This puts a HUGE limitation on files especially when lots + // have been uploaded. + $base = Director::baseFolder(); + $parentFolder = Folder::findOrMake($folderPath); + + // Create a folder for uploading. + if(!file_exists("$base/assets")){ + mkdir("$base/assets", Filesystem::$folder_create_mask); + } + if(!file_exists("$base/assets/" . $folderPath)){ + mkdir("$base/assets/" . $folderPath, Filesystem::$folder_create_mask); + } + + // Generate default filename + $fileName = str_replace(' ', '-',$tmpFile['name']); + $fileName = ereg_replace('[^A-Za-z0-9+.-]+','',$fileName); + $fileName = ereg_replace('-+', '-',$fileName); + $fileName = basename($fileName); + + $relativeFilePath = "assets/" . $folderPath . "/$fileName"; + + // if filename already exists, version the filename (e.g. test.gif to test1.gif) + while(file_exists("$base/$relativeFilePath")) { + $i = isset($i) ? ($i+1) : 2; + $oldFilePath = $relativeFilePath; + // make sure archives retain valid extensions + if(substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.gz')) == '.tar.gz' || + substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.bz2')) == '.tar.bz2') { + $relativeFilePath = ereg_replace('[0-9]*(\.tar\.[^.]+$)',$i . '\\1', $relativeFilePath); + } else { + $relativeFilePath = ereg_replace('[0-9]*(\.[^.]+$)',$i . '\\1', $relativeFilePath); + } + if($oldFilePath == $relativeFilePath && $i > 2) user_error("Couldn't fix $relativeFilePath with $i tries", E_USER_ERROR); + } + + if(file_exists($tmpFile['tmp_name']) && copy($tmpFile['tmp_name'], "$base/$relativeFilePath")) { + $this->file->ParentID = $parentFolder->ID; + // This is to prevent it from trying to rename the file + $this->file->Name = basename($relativeFilePath); + $this->file->write(); + return true; + } else { + $this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.'); + return false; + } + } + + /** + * Load temporary PHP-upload into File-object. + * + * @param array $tmpFile + * @param File $file + * @return Boolean + */ + public function loadIntoFile($tmpFile, $file) { + $this->file = $file; + + return $this->load($tmpFile); + } + + /** + * Container for all validation on the file + * (e.g. size and extension restrictions). + * Is NOT connected to the {Validator} classes, + * please have a look at {FileField->validate()} + * for an example implementation of external validation. + * + * @param array $tmpFile + * @return boolean + */ + public function validate($tmpFile) { + $pathInfo = pathinfo($tmpFile['name']); + // filesize validation + if(!$this->isValidSize($tmpFile)) { + $this->errors[] = sprintf( + _t( + 'File.TOOLARGE', + 'Filesize is too large, maximum %s allowed.', + PR_MEDIUM, + 'Argument 1: Filesize (e.g. 1MB)' + ), + File::format_size($this->getAllowedMaxFileSize($pathInfo['extension'])) + ); + return false; + } + + // extension validation + if(!$this->isValidExtension($tmpFile)) { + $this->errors[] = sprintf( + _t( + 'File.INVALIDEXTENSION', + 'Extension is not allowed (valid: %s)', + PR_MEDIUM, + 'Argument 1: Comma-separated list of valid extensions' + ), + implode(',',$this->allowedExtensions) + ); + return false; + } + + return true; + } + + /** + * Get file-object, either generated from {load()}, + * or manually set. + * + * @return File + */ + public function getFile() { + return $this->file; + } + + /** + * Set a file-object (similiar to {loadIntoFile()}) + * + * @param File $file + */ + public function setFile($file) { + $this->file = $file; + } + + /** + * Get maximum file size for all or specified file extension. + * + * @param string $ext + * @return int Filesize in bytes + */ + public function getAllowedMaxFileSize($ext = null) { + $ext = strtolower($ext); + if(isset($ext) && isset($this->allowedMaxFileSize[$ext])) { + return $this->allowedMaxFileSize[$ext]; + } else { + return (isset($this->allowedMaxFileSize['*'])) ? $this->allowedMaxFileSize['*'] : false; + } + } + + /** + * Set filesize maximums (in bytes). + * Automatically converts extensions to lowercase + * for easier matching. + * + * Example: + * + * array('*' => 200, 'jpg' => 1000) + * + * + * @param array|int $rules + */ + public function setAllowedMaxFileSize($rules) { + if(is_array($rules) && count($rules)) { + // make sure all extensions are lowercase + $rules = array_change_key_case($rules, CASE_LOWER); + $this->allowedMaxFileSize = $rules; + } elseif((int)$rules > 0) { + $this->allowedMaxFileSize['*'] = (int)$rules; + } + } + + /** + * @return array + */ + public function getAllowedExtensions() { + return $this->allowedExtensions; + } + + /** + * @param array $rules + */ + public function setAllowedExtensions($rules) { + if(!is_array($rules)) return false; + + // make sure all rules are lowercase + foreach($rules as &$rule) $rule = strtolower($rule); + + $this->allowedExtensions = $rules; + } + + /** + * Determines if the bytesize of an uploaded + * file is valid - can be defined on an + * extension-by-extension basis in {$allowedMaxFileSize} + * + * @param array $tmpFile + * @return boolean + */ + public function isValidSize($tmpFile) { + $pathInfo = pathinfo($tmpFile['name']); + $maxSize = $this->getAllowedMaxFileSize(strtolower($pathInfo['extension'])); + return (!$tmpFile['size'] || !$maxSize || (int)$tmpFile['size'] < $maxSize); + } + + /** + * Determines if the temporary file has a valid extension + * + * @param array $tmpFile + * @return boolean + */ + public function isValidExtension($tmpFile) { + $pathInfo = pathinfo($tmpFile['name']); + return (!count($this->allowedExtensions) || array_key_exists(strtolower($pathInfo['extension']), $this->allowedExtensions)); + } + + + /** + * Clear out all errors (mostly set by {loadUploaded()}) + */ + public function clearErrors() { + $this->errors = array(); + } + + /** + * Determines wether previous operations caused an error. + * + * @return boolean + */ + public function isError() { + return (count($this->errors)); + } + + /** + * Return all errors that occurred while processing so far + * (mostly set by {loadUploaded()}) + * + * @return array + */ + public function getErrors() { + return $this->errors; + } + +} +?> \ No newline at end of file diff --git a/forms/EditableFileField.php b/forms/EditableFileField.php index 2af539893..8858bf3ca 100755 --- a/forms/EditableFileField.php +++ b/forms/EditableFileField.php @@ -1,13 +1,7 @@ "File" ); - // TODO Interface and properties for these properties - static $file_size_restrictions = array(); - static $allowed_file_types = array(); + /** + * @see {Upload->allowedMaxFileSize} + * @var int + */ + public static $allowed_max_file_size; + + /** + * @see {Upload->allowedExtensions} + * @var array + */ + public static $allowed_extensions = array(); static $singular_name = 'File field'; static $plural_names = 'File fields'; - function ExtraOptions() { - return parent::ExtraOptions(); - } - function getFormField() { - if( $field = parent::getFormField() ) + if($field = parent::getFormField()) return $field; return new FileField($this->Name, $this->Title, $this->getField('Default')); // TODO We can't use the preview feature because FileIFrameField also shows the "From the file store" functionality @@ -41,8 +39,8 @@ class EditableFileField extends EditableFormField { return new FileField($this->Name, $this->Title, $this->getField('Default')); } - function createSubmittedField($data, $submittedForm, $fieldClass = "SubmittedFileField" ) { - if( !$_FILES[$this->Name] ) + function createSubmittedField($data, $submittedForm, $fieldClass = "SubmittedFileField") { + if(!$_FILES[$this->Name]) return null; $submittedField = new $fieldClass(); @@ -51,12 +49,17 @@ class EditableFileField extends EditableFormField { $submittedField->ParentID = $submittedForm->ID; // create the file from post data - $uploadedFile = new File(); - $uploadedFile->set_stat('file_size_restrictions',$this->stat('file_size_restrictions')); - $uploadedFile->set_stat('allowed_file_types',$this->stat('allowed_file_types')); - $uploadedFile->loadUploaded( $_FILES[$this->Name] ); + $upload = new Upload(); + $upload->setAllowedExtensions(self::$allowed_extensions); + $upload->setAllowedMaxFileSize(self::$allowed_max_file_size); + + // upload file + $upload->load($_FILES[$this->Name]); + + $uploadedFile = $upload->getFile(); $submittedField->UploadedFileID = $uploadedFile->ID; $submittedField->write(); + return $submittedField; } } diff --git a/forms/FileField.php b/forms/FileField.php index ce5de7f9e..391dc26f5 100755 --- a/forms/FileField.php +++ b/forms/FileField.php @@ -1,18 +1,71 @@ + * array("jpg","GIF") + * + * + * @var array + */ + public $allowedExtensions = array(); + + /** + * Flag to automatically determine and save a has_one-relationship + * on the saved record (e.g. a "Player" has_one "PlayerImage" would + * trigger saving the ID of newly created file into "PlayerImageID" + * on the record). + * + * @var unknown_type + */ + public $relationAutoSetting = true; + + /** + * Upload object (needed for validation + * and actually moving the temporary file + * created by PHP). + * + * @var Upload + */ + protected $upload; + + /** + * Partial filesystem path relative to /assets directory. + * Defaults to 'Uploads'. + * + * @var string + */ + protected $folderName = 'Uploads'; + /** * Create a new file field. + * * @param string $name The internal field name, passed to forms. * @param string $title The field label. * @param int $value The value of the field. @@ -20,33 +73,145 @@ class FileField extends FormField { * @param string $rightTitle Used in SmallFieldHolder() to force a right-aligned label * @param string $folderName Folder to upload files to */ - function __construct($name, $title = null, $value = null, $form = null, $rightTitle = null, $folderName = 'Uploads') { - $this->folderName = $folderName; + function __construct($name, $title = null, $value = null, $form = null, $rightTitle = null, $folderName = null) { + if(isset($folderName)) $this->folderName = $folderName; + $this->upload = new Upload(); parent::__construct($name, $title, $value, $form, $rightTitle); } public function Field() { return - $this->createTag("input", array("type" => "file", "name" => $this->name, "id" => $this->id())) . - $this->createTag("input", array("type" => "hidden", "name" => "MAX_FILE_SIZE", "value" => 30*1024*1024)); + $this->createTag("input", + array( + "type" => "file", + "name" => $this->name, + "id" => $this->id(), + "tabindex" => $this->getTabIndex() + ) + ) . + $this->createTag("input", + array( + "type" => "hidden", + "name" => "MAX_FILE_SIZE", + "value" => $this->getAllowedMaxFileSize(), + "tabindex" => $this->getTabIndex() + ) + ); } public function saveInto(DataObject $record) { - $fieldName = $this->name . 'ID'; - $hasOnes = $record->has_one($this->name); + if(!isset($_FILES[$this->name])) return false; - // assume that the file is connected via a has-one - if(!$hasOnes || !isset($_FILES[$this->name]) || !$_FILES[$this->name]['name']) return false; + $this->upload->setAllowedExtensions($this->allowedExtensions); + $this->upload->setAllowedMaxFileSize($this->allowedMaxFileSize); + $this->upload->load($_FILES[$this->name]); + if($this->upload->isError()) return false; - $file = new File(); - $file->loadUploaded($_FILES[$this->name], $this->folderName); + $file = $this->upload->getFile(); - $record->$fieldName = $file->ID; + if($this->relationAutoSetting) { + $fieldName = $this->name . 'ID'; + // assume that the file is connected via a has-one + $hasOnes = $record->has_one($this->name); + if(!$hasOnes) return false; + + // save to record + $record->$fieldName = $file->ID; + } } public function Value() { return $_FILES[$this->Name()]; } + + /** + * Get maximum file size for all or specified file extension. + * Falls back to the default filesize restriction ('*') + * if the extension was not found. + * + * @param string $ext + * @return int Filesize in bytes (0 means no filesize set) + */ + public function getAllowedMaxFileSize($ext = null) { + $ext = strtolower($ext); + if(isset($ext) && isset($this->allowedMaxFileSize[$ext])) { + return $this->allowedMaxFileSize[$ext]; + } else { + return (isset($this->allowedMaxFileSize['*'])) ? $this->allowedMaxFileSize['*'] : 0; + } + } + + /** + * Set filesize maximums (in bytes). + * Automatically converts extensions to lowercase + * for easier matching. + * + * Example: + * + * array('*' => 200, 'jpg' => 1000) + * + * + * @param unknown_type $rules + */ + public function setAllowedMaxFileSize($rules) { + if(is_array($rules)) { + // make sure all extensions are lowercase + $rules = array_change_key_case($rules, CASE_LOWER); + $this->allowedMaxFileSize = $rules; + } else { + $this->allowedMaxFileSize['*'] = (int)$rules; + } + } + + /** + * @return array + */ + public function getAllowedExtensions() { + return $this->allowedExtensions; + } + + /** + * @param array $rules + */ + public function setAllowedExtensions($rules) { + if(!is_array($rules)) return false; + + // make sure all rules are lowercase + foreach($rules as &$rule) $rule = strtolower($rule); + + $this->allowedExtensions = $rules; + } + + /** + * @param string $folderName + */ + public function setFolderName($folderName) { + $this->folderName = $folderName; + } + + /** + * @return string + */ + public function getFolderName() { + return $folderName; + } + + public function validate($validator) { + $tmpFile = $_FILES[$this->name]; + $this->upload->setAllowedExtensions($this->allowedExtensions); + $this->upload->setAllowedMaxFileSize($this->allowedMaxFileSize); + + $valid = $this->upload->validate($tmpFile); + if(!$valid) { + $errors = $this->upload->getErrors(); + if($errors) foreach($errors as $error) { + $validator->validationError($this->name, $error, "validation", false); + } + return false; + } + + return true; + } } ?> diff --git a/tests/filesystem/UploadTest.php b/tests/filesystem/UploadTest.php new file mode 100644 index 000000000..9297defe7 --- /dev/null +++ b/tests/filesystem/UploadTest.php @@ -0,0 +1,63 @@ + $tmpFileName, + 'type' => 'text/plaintext', + 'size' => filesize($tmpFilePath), + 'tmp_name' => $tmpFilePath, + 'extension' => 'txt', + 'error' => UPLOAD_ERR_OK, + ); + + // test upload into default folder + $u1 = new Upload(); + $u1->load($tmpFile); + $file1 = $u1->getFile(); + $this->assertTrue( + file_exists($file1->getFullPath()), + 'File upload to standard directory in /assets' + ); + $this->assertTrue( + (strpos($file1->getFullPath(),Director::baseFolder() . '/assets/' . Upload::$uploads_folder) !== false), + 'File upload to standard directory in /assets' + ); + $file1->delete(); + + // test upload into custom folder + $customFolder = 'UploadTest_testUpload'; + $u2 = new Upload(); + $u2->load($tmpFile, $customFolder); + $file2 = $u2->getFile(); + $this->assertTrue( + file_exists($file2->getFullPath()), + 'File upload to custom directory in /assets' + ); + $this->assertTrue( + (strpos($file2->getFullPath(),Director::baseFolder() . '/assets/' . $customFolder) !== false), + 'File upload to custom directory in /assets' + ); + $file2->delete(); + + unlink($tmpFilePath); + } + + function testAllowedFilesize() { + // @todo + } + + function testAllowedExtensions() { + // @todo + } + +} +?> \ No newline at end of file