diff --git a/framework/validators/CFileValidator.php b/framework/validators/CFileValidator.php index 5fd5c3b94..76216b619 100644 --- a/framework/validators/CFileValidator.php +++ b/framework/validators/CFileValidator.php @@ -15,14 +15,25 @@ * about the uploaded file. It then checks if a file is uploaded successfully, * if the file size is within the limit and if the file type is allowed. * + * This validator will attempt to fetch uploaded data if attribute is not + * previously set. Please note that this cannot be done if input is tabular: + *
+ *  foreach($models as $i=>$model)
+ *     $model->attribute = CUploadedFile::getInstance($model, "[$i]attribute");
+ * 
+ * Please note that you must use {link CUploadedFile::getInstances} for multiple + * file uploads. + * * When using CFileValidator with an active record, the following code is often used: *
- *  // assuming the upload file field is generated using
- *  // CHtml::activeFileField($model,'file');
- *  $model->file=CUploadedFile::getInstance($model,'file');
- *  $model->fileSize=$file->size;
  *  if($model->save())
- *      $model->file->saveAs($path); // save the uploaded file
+ *  {
+ *     // single upload
+ *     $model->attribute->saveAs($path);
+ *     // multiple upload
+ *     foreach($model->attribute as $file)
+ *        $file->saveAs($path);
+ *  }
  * 
* * You can use {@link CFileValidator} to validate the file attribute. @@ -76,68 +87,112 @@ class CFileValidator extends CValidator * that is not listed among {@link extensions}. */ public $wrongType; + /** + * @var integer the maximum file count the given attribute can hold. + * It defaults to 1, meaning single file upload. By defining a higher number, + * multiple uploads become possible. + */ + public $maxFiles=1; + /** + * @var string the error message used if the count of multiple uploads exceeds + * limit. + */ + public $tooMany; /** - * Validates the attribute of the object. + * Set the attribute and then validates using {@link validateFile}. * If there is any error, the error message is added to the object. * @param CModel the object being validated * @param string the attribute being validated */ - protected function validateAttribute($object,$attribute) + protected function validateAttribute($object, $attribute) { - $files=$object->$attribute; - if($files instanceof CUploadedFile) - $files=array($files); - if(!is_array($files)) - $files=CUploadedFile::getInstance($object,$attribute); - - foreach($files as $file) + if($this->maxFiles > 1) { - $error=$file->getError(); - if($error==UPLOAD_ERR_NO_FILE) + if(null===$object->$attribute) + $object->$attribute = CUploadedFile::getInstances($object, $attribute); + if(array()===$object->$attribute) + return $this->emptyAttribute($object, $attribute); + if(count($object->$attribute) > $this->maxFiles) { - if(!$this->allowEmpty) - { - $message=$this->message!==null?$this->message : Yii::t('yii','{attribute} cannot be blank.'); - $this->addError($object,$attribute,$message); - } - return; // regardless of other uploads + $message=$this->tooMany!==null?$this->tooMany : Yii::t('yii', '{attribute} cannot accept more than {limit} files.'); + $this->addError($object, $attribute, $message, array('{attribute}'=>$attribute, '{limit}'=>$this->maxFiles)); } - else if($error==UPLOAD_ERR_INI_SIZE || $error==UPLOAD_ERR_FORM_SIZE || $this->maxSize!==null && $file->getSize()>$this->maxSize) + else + foreach($object->$attribute as $file) + $this->validateFile($object, $attribute, $file); + } + else + { + if(null===$object->$attribute) { - $message=$this->tooLarge!==null?$this->tooLarge : Yii::t('yii','The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); - $this->addError($object,$attribute,$message,array('{file}'=>$file->getName(), '{limit}'=>$this->getSizeLimit())); + $file = CUploadedFile::getInstance($object, $attribute); + if(null===$file) + return $this->emptyAttribute($object, $attribute); + $object->$attribute = $file; } - else if($error==UPLOAD_ERR_PARTIAL) - throw new CException(Yii::t('yii','The file "{file}" was only partially uploaded.',array('{file}'=>$file->getName()))); - else if($error==UPLOAD_ERR_NO_TMP_DIR) - throw new CException(Yii::t('yii','Missing the temporary folder to store the uploaded file "{file}".',array('{file}'=>$file->getName()))); - else if($error==UPLOAD_ERR_CANT_WRITE) - throw new CException(Yii::t('yii','Failed to write the uploaded file "{file}" to disk.',array('{file}'=>$file->getName()))); - else if(defined('UPLOAD_ERR_EXTENSION') && $error==UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above - throw new CException(Yii::t('yii','File upload was stopped by extension.')); + $this->validateFile($object, $attribute, $object->$attribute); + } + } - if($this->minSize!==null && $file->getSize()<$this->minSize) - { - $message=$this->tooSmall!==null?$this->tooSmall : Yii::t('yii','The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); - $this->addError($object,$attribute,$message,array('{file}'=>$file->getName(), '{limit}'=>$this->minSize)); - } + /** + * Internally validates a file object. + * @param CModel the object being validated + * @param string the attribute being validated + * @param CUploadedFile uploaded file passed to check against a set of rules + */ + protected function validateFile($object, $attribute, $file) + { + if(null===$file || ($error=$file->getError())==UPLOAD_ERR_NO_FILE) + return $this->emptyAttribute($object, $attribute); + else if($error==UPLOAD_ERR_INI_SIZE || $error==UPLOAD_ERR_FORM_SIZE || $this->maxSize!==null && $file->getSize()>$this->maxSize) + { + $message=$this->tooLarge!==null?$this->tooLarge : Yii::t('yii','The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); + $this->addError($object,$attribute,$message,array('{file}'=>$file->getName(), '{limit}'=>$this->getSizeLimit())); + } + else if($error==UPLOAD_ERR_PARTIAL) + throw new CException(Yii::t('yii','The file "{file}" was only partially uploaded.',array('{file}'=>$file->getName()))); + else if($error==UPLOAD_ERR_NO_TMP_DIR) + throw new CException(Yii::t('yii','Missing the temporary folder to store the uploaded file "{file}".',array('{file}'=>$file->getName()))); + else if($error==UPLOAD_ERR_CANT_WRITE) + throw new CException(Yii::t('yii','Failed to write the uploaded file "{file}" to disk.',array('{file}'=>$file->getName()))); + else if(defined('UPLOAD_ERR_EXTENSION') && $error==UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above + throw new CException(Yii::t('yii','File upload was stopped by extension.')); - if($this->types!==null) + if($this->minSize!==null && $file->getSize()<$this->minSize) + { + $message=$this->tooSmall!==null?$this->tooSmall : Yii::t('yii','The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); + $this->addError($object,$attribute,$message,array('{file}'=>$file->getName(), '{limit}'=>$this->minSize)); + } + + if($this->types!==null) + { + if(is_string($this->types)) + $types=preg_split('/[\s,]+/',strtolower($this->types),-1,PREG_SPLIT_NO_EMPTY); + else + $types=$this->types; + if(!in_array(strtolower($file->getExtensionName()),$types)) { - if(is_string($this->types)) - $types=preg_split('/[\s,]+/',strtolower($this->types),-1,PREG_SPLIT_NO_EMPTY); - else - $types=$this->types; - if(!in_array(strtolower($file->getExtensionName()),$types)) - { - $message=$this->wrongType!==null?$this->wrongType : Yii::t('yii','The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); - $this->addError($object,$attribute,$message,array('{file}'=>$file->getName(), '{extensions}'=>implode(', ',$types))); - } + $message=$this->wrongType!==null?$this->wrongType : Yii::t('yii','The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); + $this->addError($object,$attribute,$message,array('{file}'=>$file->getName(), '{extensions}'=>implode(', ',$types))); } } } + /** + * Raises an error to inform end user about blank attribute. + * @param CModel the object being validated + * @param string the attribute being validated + */ + protected function emptyAttribute($object, $attribute) + { + if(!$this->allowEmpty) + { + $message=$this->message!==null?$this->message : Yii::t('yii','{attribute} cannot be blank.'); + $this->addError($object,$attribute,$message); + } + } + /** * Returns the maximum size allowed for uploaded files. * This is determined based on three factors: diff --git a/framework/web/CUploadedFile.php b/framework/web/CUploadedFile.php index 6301e1025..2746de22d 100644 --- a/framework/web/CUploadedFile.php +++ b/framework/web/CUploadedFile.php @@ -35,52 +35,79 @@ class CUploadedFile extends CComponent * Returns an instance of the specified uploaded file. * The file should be uploaded using {@link CHtml::activeFileField}. * @param CModel the model instance - * @param string the attribute name. For tabular file uploading, this can be in the format of "attributeName[$i]", where $i stands for an integer index. + * @param string the attribute name. For tabular file uploading, this can be in the format of "[$i]attributeName", where $i stands for an integer index. * @return CUploadedFile the instance of the uploaded file. * Null is returned if no file is uploaded for the specified model attribute. * @see getInstanceByName */ - public static function getInstance($model,$attribute) + public static function getInstance($model, $attribute) { - return self::getInstanceByName(CHtml::resolveName($model, $attribute), true); + return self::getInstanceByName(CHtml::resolveName($model, $attribute)); + } + + /** + * Returns all uploaded files for the given model attribute. + * @param CModel the model instance + * @param string the attribute name. For tabular file uploading, this can be in the format of "[$i]attributeName", where $i stands for an integer index. + * @return array array of CUploadedFile objects. + * Empty array is returned if no available file was found for the given attribute. + */ + public static function getInstances($model, $attribute) + { + return self::getInstancesByName(CHtml::resolveName($model, $attribute)); } /** * Returns an instance of the specified uploaded file. * The name can be a plain string or a string like an array element (e.g. 'Post[imageFile]', or 'Post[0][imageFile]'). * @param string the name of the file input field. - * @param boolean whether search should be initiated in possible subarrays * @return CUploadedFile the instance of the uploaded file. * Null is returned if no file is uploaded for the specified name. - * An array of CUploadedFile is returned if search is needed, but - * empty array is returned if no file is available. */ - public static function getInstanceByName($name, $search=false) + public static function getInstanceByName($name) { if(null===self::$_files) - { - self::$_files = array(); + self::prefetchFiles(); - if(!isset($_FILES) || !is_array($_FILES)) - return null; - - foreach($_FILES as $class=>$info) - self::collectFiles($class, $info['name'], $info['tmp_name'], $info['type'], $info['type'], $info['size'], $info['error']); - } - - if($search) - { - $len=strlen($name); - $results=array(); - foreach(array_keys(self::$_files) as $key) - if(0===strncmp($key, $name, $len)) - $results[] = self::$_files[$key]; - return $results; - } - else - return isset(self::$_files[$name]) && self::$_files[$name]->getError()!=UPLOAD_ERR_NO_FILE ? self::$_files[$name] : null; + return isset(self::$_files[$name]) && self::$_files[$name]->getError()!=UPLOAD_ERR_NO_FILE ? self::$_files[$name] : null; } + /** + * Returns an array of instances for the specified array name. + * + * If multiple files were uploaded and saved as 'Files[0]', 'Files[1]', + * 'Files[n]'..., you can have them all by passing 'Files' as array name. + * @param string the name of the array of files + * @return array the array of CUploadedFile objects. Empty array is returned + * if no adequate upload was found. Please note that this array will contain + * all files from all subarrays regardless how deeply nested they are. + */ + public static function getInstancesByName($name) + { + if(null===self::$_files) + self::prefetchFiles(); + + $len=strlen($name); + $results=array(); + foreach(array_keys(self::$_files) as $key) + if(0===strncmp($key, $name, $len) && self::$_files[$key]->getError()!=UPLOAD_ERR_NO_FILE) + $results[] = self::$_files[$key]; + return $results; + } + + /** + * Initially processes $_FILES superglobal for easier use. + * Only for internal usage. + */ + protected static function prefetchFiles() + { + self::$_files = array(); + if(!isset($_FILES) || !is_array($_FILES)) + return; + + foreach($_FILES as $class=>$info) + self::collectFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']); + } /** * Processes incoming files for {@link getInstanceByName}. * @param string key for identifiing uploaded file: class name and subarray indexes @@ -90,12 +117,12 @@ class CUploadedFile extends CComponent * @param mixed file sizes provided by PHP * @param mixed uploading issues provided by PHP */ - protected static function collectFiles($key, $names, $tmp_names, $types, $sizes, $errors) + protected static function collectFilesRecursive($key, $names, $tmp_names, $types, $sizes, $errors) { if(is_array($names)) { foreach($names as $item=>$name) - self::collectFiles($key.'['.$item.']', $names[$item], $tmp_names[$item], $types[$item], $sizes[$item], $errors[$item]); + self::collectFilesRecursive($key.'['.$item.']', $names[$item], $tmp_names[$item], $types[$item], $sizes[$item], $errors[$item]); } else self::$_files[$key] = new CUploadedFile($names, $tmp_names, $types, $sizes, $errors);