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