Version: current

      1 <?php
      2 /**
      3  * Manages uploads via HTML forms processed by PHP,
      4  * uploads to Silverstripe's default upload directory,
      5  * and either creates a new or uses an existing File-object
      6  * for syncing with the database.
      7  *
      8  * @package sapphire
      9  * @subpackage filesystem
     10  *
     11  * @todo Allow for non-database uploads
     12  */
     13 class Upload extends Controller {
     14
     15     /**
     16      * A File object
     17      * @var File
     18      */
     19     protected $file;
     20
     21     /**
     22      * An instance of Upload_Validator
     23      * @var Upload_Validator
     24      */
     25     protected $validator;
     26
     27     /**
     28      * Information about the temporary file produced
     29      * by the PHP-runtime.
     30      *
     31      * @var array
     32      */
     33     protected $tmpFile;
     34
     35     /**
     36      * Processing errors that can be evaluated,
     37      * e.g. by Form-validation.
     38      *
     39      * @var array
     40      */
     41     protected $errors = array();
     42
     43     /**
     44      * A foldername relative to /assets,
     45      * where all uploaded files are stored by default.
     46      *
     47      * @var string
     48      */
     49     public static $uploads_folder = "Uploads";
     50
     51     public function __construct() {
     52         parent::__construct();
     53         $this->validator = new Upload_Validator();
     54     }
     55
     56     /**
     57      * Get current validator
     58      *
     59      * @return object $validator
     60      */
     61     public function getValidator() {
     62         return $this->validator;
     63     }
     64
     65     /**
     66      * Set a different instance than {@link Upload_Validator}
     67      * for this upload session.
     68      *
     69      * @param object $validator
     70      */
     71     public function setValidator($validator) {
     72         $this->validator = $validator;
     73     }
     74
     75     /**
     76      * Save an file passed from a form post into this object.
     77      *
     78      * @param $tmpFile array Indexed array that PHP generated for every file it uploads.
     79      * @param $folderPath string Folder path relative to /assets
     80      * @return Boolean|string Either success or error-message.
     81      */
     82     function load($tmpFile, $folderPath = false) {
     83         $this->clearErrors();
     84
     85         if(!$folderPath) $folderPath = self::$uploads_folder;
     86
     87         if(!$this->file) $this->file = new File();
     88
     89         if(!is_array($tmpFile)) {
     90             user_error("Upload::load() Not passed an array.  Most likely, the form hasn't got the right enctype", E_USER_ERROR);
     91         }
     92
     93         if(!$tmpFile['size']) {
     94             $this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
     95             return false;
     96         }
     97
     98         $valid = $this->validate($tmpFile);
     99         if(!$valid) return false;
    100
    101         // @TODO This puts a HUGE limitation on files especially when lots
    102         // have been uploaded.
    103         $base = Director::baseFolder();
    104         $parentFolder = Folder::findOrMake($folderPath);
    105
    106         // Create a folder for uploading.
    107         if(!file_exists(ASSETS_PATH)){
    108             mkdir(ASSETS_PATH, Filesystem::$folder_create_mask);
    109         }
    110         if(!file_exists(ASSETS_PATH . "/" . $folderPath)){
    111             mkdir(ASSETS_PATH . "/" . $folderPath, Filesystem::$folder_create_mask);
    112         }
    113
    114         // Generate default filename
    115         $fileName = Convert::rus2lat($tmpFile['name']);
    116         $fileName = ereg_replace('^[_.-]+', '',$fileName);
    117         $fileName = ereg_replace('[+\ ]+', '-',$fileName);
    118         $fileName = ereg_replace('[^A-Za-z0-9._-]+','',$fileName);
    119         $fileName = ereg_replace('-+', '-',$fileName);
    120         $fileName = basename($fileName);
    121
    122         $relativeFilePath = ASSETS_DIR . "/" . $folderPath . "/$fileName";
    123
    124         // if filename already exists, version the filename (e.g. test.gif to test1.gif)
    125         while(file_exists("$base/$relativeFilePath")) {
    126             $i = isset($i) ? ($i+1) : 2;
    127             $oldFilePath = $relativeFilePath;
    128             // make sure archives retain valid extensions
    129             if(substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.gz')) == '.tar.gz' ||
    130                 substr($relativeFilePath, strlen($relativeFilePath) - strlen('.tar.bz2')) == '.tar.bz2') {
    131                     $relativeFilePath = ereg_replace('[0-9]*(\.tar\.[^.]+$)',$i . '\\1', $relativeFilePath);
    132             } else if (strpos($relativeFilePath, '.') !== false) {
    133                 $relativeFilePath = ereg_replace('[0-9]*(\.[^.]+$)',$i . '\\1', $relativeFilePath);
    134             } else if (strpos($relativeFilePath, '_') !== false) {
    135                 $relativeFilePath = ereg_replace('_([^_]+$)', '_'.$i, $relativeFilePath);
    136             } else {
    137                 $relativeFilePath .= "_$i";
    138             }
    139             if($oldFilePath == $relativeFilePath && $i > 2) user_error("Couldn't fix $relativeFilePath with $i tries", E_USER_ERROR);
    140         }
    141
    142         if(file_exists($tmpFile['tmp_name']) && move_uploaded_file($tmpFile['tmp_name'], "$base/$relativeFilePath")) {
    143             $this->file->ParentID = $parentFolder->ID;
    144             // This is to prevent it from trying to rename the file
    145             $this->file->Name = basename($relativeFilePath);
    146             $this->file->Title = basename($tmpFile['name']);
    147             $this->file->write();
    148             return true;
    149         } else {
    150             $this->errors[] = _t('File.NOFILESIZE', 'Filesize is zero bytes.');
    151             return false;
    152         }
    153     }
    154
    155     /**
    156      * Load temporary PHP-upload into File-object.
    157      *
    158      * @param array $tmpFile
    159      * @param File $file
    160      * @return Boolean
    161      */
    162     public function loadIntoFile($tmpFile, $file, $folderPath = false) {
    163         $this->file = $file;
    164         return $this->load($tmpFile, $folderPath);
    165     }
    166
    167     /**
    168      * Container for all validation on the file
    169      * (e.g. size and extension restrictions).
    170      * Is NOT connected to the {Validator} classes,
    171      * please have a look at {FileField->validate()}
    172      * for an example implementation of external validation.
    173      *
    174      * @param array $tmpFile
    175      * @return boolean
    176      */
    177     public function validate($tmpFile) {
    178         $validator = $this->validator;
    179         $validator->setTmpFile($tmpFile);
    180         $isValid = $validator->validate();
    181         if($validator->getErrors()) {
    182             $this->errors = array_merge($this->errors, $validator->getErrors());
    183         }
    184         return $isValid;
    185     }
    186
    187     /**
    188      * Get file-object, either generated from {load()},
    189      * or manually set.
    190      *
    191      * @return File
    192      */
    193     public function getFile() {
    194         return $this->file;
    195     }
    196
    197     /**
    198      * Set a file-object (similiar to {loadIntoFile()})
    199      *
    200      * @param File $file
    201      */
    202     public function setFile($file) {
    203         $this->file = $file;
    204     }
    205
    206     /**
    207      * Get maximum file size for all or specified file extension.
    208      *
    209      * @deprecated 2.5 Please use Upload_Validator::getAllowedMaxFileSize() instead
    210      *
    211      * @param string $ext
    212      * @return int Filesize in bytes
    213      */
    214     public function getAllowedMaxFileSize($ext = null) {
    215         user_error('Upload::getAllowedMaxFileSize() is deprecated. Please use Upload_Validator::getAllowedMaxFileSize() instead', E_USER_NOTICE);
    216         return $this->validator->getAllowedMaxFileSize($ext);
    217     }
    218
    219     /**
    220      * Set filesize maximums (in bytes).
    221      * Automatically converts extensions to lowercase
    222      * for easier matching.
    223      *
    224      * Example:
    225      * <code>
    226      * array('*' => 200, 'jpg' => 1000)
    227      * </code>
    228      *
    229      * @deprecated 2.5 Please use Upload_Validator::setAllowedMaxFileSize() instead
    230      *
    231      * @param array|int $rules
    232      */
    233     public function setAllowedMaxFileSize($rules) {
    234         user_error('Upload::setAllowedMaxFileSize() is deprecated. Please use Upload_Validator::setAllowedMaxFileSize() instead', E_USER_NOTICE);
    235         $this->validator->setAllowedMaxFileSize($rules);
    236     }
    237
    238     /**
    239      * @deprecated 2.5 Please use Upload_Validator::getAllowedExtensions() instead
    240      * @return array
    241      */
    242     public function getAllowedExtensions() {
    243         user_error('Upload::getAllowedExtensions() is deprecated. Please use Upload_Validator::getAllowedExtensions() instead', E_USER_NOTICE);
    244         return $this->validator->getAllowedExtensions();
    245     }
    246
    247     /**
    248      * @deprecated 2.5 Please use Upload_Validator::setAllowedExtensions() instead
    249      * @param array $rules
    250      */
    251     public function setAllowedExtensions($rules) {
    252         user_error('Upload::setAllowedExtensions() is deprecated. Please use Upload_Validator::setAllowedExtensions() instead', E_USER_NOTICE);
    253         $this->validator->setAllowedExtensions($rules);
    254     }
    255
    256     /**
    257      * Determines if the bytesize of an uploaded
    258      * file is valid - can be defined on an
    259      * extension-by-extension basis in {$allowedMaxFileSize}
    260      *
    261      * @deprecated 2.5 Please use Upload_Validator::isValidExtension() instead
    262      *
    263      * @param array $tmpFile
    264      * @return boolean
    265      */
    266     public function isValidSize($tmpFile) {
    267         user_error('Upload::isValidSize() is deprecated. Please use Upload_Validator::isValidSize() instead', E_USER_NOTICE);
    268         $validator = new Upload_Validator();
    269         $validator->setTmpFile($tmpFile);
    270         return $validator->isValidSize();
    271     }
    272
    273     /**
    274      * Determines if the temporary file has a valid extension
    275      *
    276      * @deprecated 2.5 Please use Upload_Validator::isValidExtension() instead
    277      *
    278      * @param array $tmpFile
    279      * @return boolean
    280      */
    281     public function isValidExtension($tmpFile) {
    282         user_error('Upload::isValidExtension() is deprecated. Please use Upload_Validator::isValidExtension() instead', E_USER_NOTICE);
    283         $validator = new Upload_Validator();
    284         $validator->setTmpFile($tmpFile);
    285         return $validator->isValidExtension();
    286     }
    287
    288     /**
    289      * Clear out all errors (mostly set by {loadUploaded()})
    290      */
    291     public function clearErrors() {
    292         $this->errors = array();
    293     }
    294
    295     /**
    296      * Determines wether previous operations caused an error.
    297      *
    298      * @return boolean
    299      */
    300     public function isError() {
    301         return (count($this->errors));
    302     }
    303
    304     /**
    305      * Return all errors that occurred while processing so far
    306      * (mostly set by {loadUploaded()})
    307      *
    308      * @return array
    309      */
    310     public function getErrors() {
    311         return $this->errors;
    312     }
    313
    314 }
    315
    316 /**
    317  * @package sapphire
    318  * @subpackage filesystem
    319  */
    320 class Upload_Validator {
    321
    322     /**
    323      * Information about the temporary file produced
    324      * by the PHP-runtime.
    325      *
    326      * @var array
    327      */
    328     protected $tmpFile;
    329
    330     protected $errors = array();
    331
    332     /**
    333      * Restrict filesize for either all filetypes
    334      * or a specific extension, with extension-name
    335      * as array-key and the size-restriction in bytes as array-value.
    336      *
    337      * @var array
    338      */
    339     public $allowedMaxFileSize = array();
    340
    341     /**
    342      * @var array Collection of extensions.
    343      * Extension-names are treated case-insensitive.
    344      *
    345      * Example:
    346      * <code>
    347      *  array("jpg","GIF")
    348      * </code>
    349      */
    350     public $allowedExtensions = array();
    351
    352     /**
    353      * Return all errors that occurred while validating
    354      * the temporary file.
    355      *
    356      * @return array
    357      */
    358     public function getErrors() {
    359         return $this->errors;
    360     }
    361
    362     /**
    363      * Set information about temporary file produced by PHP.
    364      * @param array $tmpFile
    365      */
    366     public function setTmpFile($tmpFile) {
    367         $this->tmpFile = $tmpFile;
    368     }
    369
    370     /**
    371      * Get maximum file size for all or specified file extension.
    372      *
    373      * @param string $ext
    374      * @return int Filesize in bytes
    375      */
    376     public function getAllowedMaxFileSize($ext = null) {
    377         $ext = strtolower($ext);
    378         if(isset($ext) && isset($this->allowedMaxFileSize[$ext])) {
    379             return $this->allowedMaxFileSize[$ext];
    380         } else {
    381             return (isset($this->allowedMaxFileSize['*'])) ? $this->allowedMaxFileSize['*'] : false;
    382         }
    383     }
    384
    385     /**
    386      * Set filesize maximums (in bytes).
    387      * Automatically converts extensions to lowercase
    388      * for easier matching.
    389      *
    390      * Example:
    391      * <code>
    392      * array('*' => 200, 'jpg' => 1000)
    393      * </code>
    394      *
    395      * @param array|int $rules
    396      */
    397     public function setAllowedMaxFileSize($rules) {
    398         if(is_array($rules) && count($rules)) {
    399             // make sure all extensions are lowercase
    400             $rules = array_change_key_case($rules, CASE_LOWER);
    401             $this->allowedMaxFileSize = $rules;
    402         } elseif((int) $rules > 0) {
    403             $this->allowedMaxFileSize['*'] = (int)$rules;
    404         }
    405     }
    406
    407     /**
    408      * @return array
    409      */
    410     public function getAllowedExtensions() {
    411         return $this->allowedExtensions;
    412     }
    413
    414     /**
    415      * @param array $rules
    416      */
    417     public function setAllowedExtensions($rules) {
    418         if(!is_array($rules)) return false;
    419
    420         // make sure all rules are lowercase
    421         foreach($rules as &$rule) $rule = strtolower($rule);
    422
    423         $this->allowedExtensions = $rules;
    424     }
    425
    426     /**
    427      * Determines if the bytesize of an uploaded
    428      * file is valid - can be defined on an
    429      * extension-by-extension basis in {$allowedMaxFileSize}
    430      *
    431      * @return boolean
    432      */
    433     public function isValidSize() {
    434         $pathInfo = pathinfo($this->tmpFile['name']);
    435         $extension = isset($pathInfo['extension']) ? strtolower($pathInfo['extension']) : null;
    436         $maxSize = $this->getAllowedMaxFileSize($extension);
    437         return (!$this->tmpFile['size'] || !$maxSize || (int) $this->tmpFile['size'] < $maxSize);
    438     }
    439
    440     /**
    441      * Determines if the temporary file has a valid extension
    442      * An empty string in the validation map indicates files without an extension.
    443      * @return boolean
    444      */
    445     public function isValidExtension() {
    446         $pathInfo = pathinfo($this->tmpFile['name']);
    447
    448         // Special case for filenames without an extension
    449         if(!isset($pathInfo['extension'])) {
    450             return in_array('', $this->allowedExtensions, true);
    451         } else {
    452             return (!count($this->allowedExtensions) || in_array(strtolower($pathInfo['extension']), $this->allowedExtensions));
    453         }
    454     }
    455
    456     /**
    457      * Run through the rules for this validator checking against
    458      * the temporary file set by {@link setTmpFile()} to see if
    459      * the file is deemed valid or not.
    460      *
    461      * @return boolean
    462      */
    463     public function validate() {
    464         // we don't validate for empty upload fields yet
    465         if(!isset($this->tmpFile['name']) || empty($this->tmpFile['name'])) return true;
    466
    467                 if(isset($this->tmpFile['tmp_name']) && !ini_get("open_basedir") && !is_uploaded_file($this->tmpFile['tmp_name']) && !SapphireTest::is_running_test()) {
    468             $this->errors[] = _t('File.NOVALIDUPLOAD', 'File is not a valid upload');
    469             return false;
    470         }
    471
    472         $pathInfo = pathinfo($this->tmpFile['name']);
    473         // filesize validation
    474         if(!$this->isValidSize()) {
    475             $ext = (isset($pathInfo['extension'])) ? $pathInfo['extension'] : '';
    476             $arg = File::format_size($this->getAllowedMaxFileSize($ext));
    477             $this->errors[] = sprintf(
    478                 _t(
    479                     'File.TOOLARGE',
    480                     'Filesize is too large, maximum %s allowed.',
    481                     PR_MEDIUM,
    482                     'Argument 1: Filesize (e.g. 1MB)'
    483                 ),
    484                 $arg
    485             );
    486             return false;
    487         }
    488
    489         // extension validation
    490         if(!$this->isValidExtension()) {
    491             $this->errors[] = sprintf(
    492                 _t(
    493                     'File.INVALIDEXTENSION',
    494                     'Extension %s is not allowed (valid: %s)',
    495                     PR_MEDIUM,
    496                     'Argument 1: Comma-separated list of valid extensions'
    497                 ),
    498                 $pathInfo['extension'],
    499                 implode(',', $this->allowedExtensions)
    500             );
    501             return false;
    502         }
    503
    504         return true;
    505     }
    506
    507 }

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -