Browse Source

Finished FileValidator and UploadedFile.

Added Model::formName().
tags/2.0.0-beta
Qiang Xue 12 years ago
parent
commit
bb5b6a4191
  1. 22
      framework/base/Model.php
  2. 283
      framework/validators/FileValidator.php
  3. 246
      framework/web/UploadedFile.php
  4. 63
      framework/widgets/ActiveForm.php

22
framework/base/Model.php

@ -8,8 +8,8 @@
namespace yii\base;
use yii\helpers\StringHelper;
use yii\validators\Validator;
use yii\validators\RequiredValidator;
use yii\validators\Validator;
/**
* Model is the base class for data models.
@ -169,6 +169,26 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess
}
/**
* Returns the form name that this model class should use.
*
* The form name is mainly used by [[\yii\web\ActiveForm]] to determine how to name
* the input fields for the attributes in a model. If the form name is "A" and an attribute
* name is "b", then the corresponding input name would be "A[b]". If the form name is
* an empty string, then the input name would be "b".
*
* By default, this method returns the model class name (without the namespace part)
* as the form name. You may override it when the model is used in different forms.
*
* @return string the form name of this model class.
*/
public function formName()
{
$class = get_class($this);
$pos = strrpos($class, '\\');
return $pos === false ? $class : substr($class, $pos + 1);
}
/**
* Returns the list of attribute names.
* By default, this method returns all public non-static properties of the class.
* You may override this method to change the default behavior.

283
framework/validators/FileValidator.php

@ -7,47 +7,19 @@
namespace yii\validators;
use Yii;
use yii\helpers\FileHelper;
use yii\web\UploadedFile;
/**
* CFileValidator verifies if an attribute is receiving a valid uploaded file.
*
* It uses the model class and attribute name to retrieve the information
* 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:
* <pre>
* foreach($models as $i=>$model)
* $model->attribute = CUploadedFile::getInstance($model, "[$i]attribute");
* </pre>
* 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:
* <pre>
* if($model->save())
* {
* // single upload
* $model->attribute->saveAs($path);
* // multiple upload
* foreach($model->attribute as $file)
* $file->saveAs($path);
* }
* </pre>
*
* You can use {@link CFileValidator} to validate the file attribute.
* FileValidator verifies if an attribute is receiving a valid uploaded file.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class CFileValidator extends Validator
class FileValidator extends Validator
{
/**
* @var boolean whether the attribute requires a file to be uploaded or not.
* Defaults to false, meaning a file is required to be uploaded.
*/
public $allowEmpty = false;
/**
* @var mixed a list of file name extensions that are allowed to be uploaded.
* This can be either an array or a string consisting of file extension names
* separated by space or comma (e.g. "gif, jpg").
@ -66,136 +38,179 @@ class CFileValidator extends Validator
* Defaults to null, meaning no limit.
* Note, the size limit is also affected by 'upload_max_filesize' INI setting
* and the 'MAX_FILE_SIZE' hidden field value.
* @see tooLarge
* @see tooBig
*/
public $maxSize;
/**
* @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 when a file is not uploaded correctly.
*/
public $message;
/**
* @var string the error message used when no file is uploaded.
*/
public $uploadRequired;
/**
* @var string the error message used when the uploaded file is too large.
* @see maxSize
* You may use the following tokens in the message:
*
* - {attribute}: the attribute name
* - {file}: the uploaded file name
* - {limit}: the maximum size allowed (see [[getSizeLimit()]])
*/
public $tooLarge;
public $tooBig;
/**
* @var string the error message used when the uploaded file is too small.
* @see minSize
* You may use the following tokens in the message:
*
* - {attribute}: the attribute name
* - {file}: the uploaded file name
* - {limit}: the value of [[minSize]]
*/
public $tooSmall;
/**
* @var string the error message used when the uploaded file has an extension name
* that is not listed among {@link extensions}.
* that is not listed in [[extensions]]. You may use the following tokens in the message:
*
* - {attribute}: the attribute name
* - {extensions}: the list of the allowed 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.
* @var string the error message used if the count of multiple uploads exceeds limit.
* You may use the following tokens in the message:
*
* - {attribute}: the attribute name
* - {file}: the uploaded file name
* - {limit}: the value of [[maxFiles]]
*/
public $tooMany;
/**
* Set the attribute and then validates using {@link validateFile}.
* If there is any error, the error message is added to the object.
* @param \yii\base\Model $object the object being validated
* @param string $attribute the attribute being validated
* Initializes the validator.
*/
public function validateAttribute($object, $attribute)
public function init()
{
if ($this->maxFiles > 1)
{
$files = $object->$attribute;
if (!is_array($files) || !isset($files[0]) || !$files[0] instanceof CUploadedFile)
$files = CUploadedFile::getInstances($object, $attribute);
if (array() === $files)
return $this->emptyAttribute($object, $attribute);
if (count($files) > $this->maxFiles)
{
$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
foreach ($files as $file)
$this->validateFile($object, $attribute, $file);
} else
{
$file = $object->$attribute;
if (!$file instanceof CUploadedFile)
{
$file = CUploadedFile::getInstance($object, $attribute);
if (null === $file)
return $this->emptyAttribute($object, $attribute);
}
$this->validateFile($object, $attribute, $file);
parent::init();
if ($this->message === null) {
$this->message = Yii::t('yii|File upload failed.');
}
if ($this->uploadRequired === null) {
$this->uploadRequired = Yii::t('yii|Please upload a file.');
}
if ($this->tooMany === null) {
$this->tooMany = Yii::t('yii|You can upload at most {limit} files.');
}
if ($this->wrongType === null) {
$this->wrongType = Yii::t('yii|Only files with these extensions are allowed: {extensions}.');
}
if ($this->tooBig === null) {
$this->tooBig = Yii::t('yii|The file "{file}" is too big. Its size cannot exceed {limit} bytes.');
}
if ($this->tooSmall === null) {
$this->tooSmall = Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.');
}
if (!is_array($this->types)) {
$this->types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY);
}
}
/**
* Internally validates a file object.
* Validates the attribute.
* @param \yii\base\Model $object the object being validated
* @param string $attribute the attribute being validated
* @param CUploadedFile $file uploaded file passed to check against a set of rules
*/
public function validateFile($object, $attribute, $file)
public function validateAttribute($object, $attribute)
{
if (null === $file || ($error = $file->getError()) == UPLOAD_ERR_NO_FILE)
return $this->emptyAttribute($object, $attribute);
elseif ($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()));
} elseif ($error == UPLOAD_ERR_PARTIAL)
throw new CException(\Yii::t('yii|The file "{file}" was only partially uploaded.', array('{file}' => $file->getName())));
elseif ($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())));
elseif ($error == UPLOAD_ERR_CANT_WRITE)
throw new CException(\Yii::t('yii|Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName())));
elseif (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->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))
{
$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)));
if ($this->maxFiles > 1) {
$files = $object->$attribute;
if (!is_array($files)) {
$this->addError($object, $attribute, $this->uploadRequired);
return;
}
foreach ($files as $i => $file) {
if (!$file instanceof UploadedFile || $file->getError() == UPLOAD_ERR_NO_FILE) {
unset($files[$i]);
}
}
$object->$attribute = array_values($files);
if ($files === array()) {
$this->addError($object, $attribute, $this->uploadRequired);
}
if (count($files) > $this->maxFiles) {
$this->addError($object, $attribute, $this->tooMany, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles));
} else {
foreach ($files as $file) {
$this->validateFile($object, $attribute, $file);
}
}
} else {
$file = $object->$attribute;
if ($file instanceof UploadedFile && $file->getError() != UPLOAD_ERR_NO_FILE) {
$this->validateFile($object, $attribute, $file);
} else {
$this->addError($object, $attribute, $this->uploadRequired);
}
}
}
/**
* Raises an error to inform end user about blank attribute.
* Internally validates a file object.
* @param \yii\base\Model $object the object being validated
* @param string $attribute the attribute being validated
* @param UploadedFile $file uploaded file passed to check against a set of rules
*/
public function emptyAttribute($object, $attribute)
protected function validateFile($object, $attribute, $file)
{
if (!$this->allowEmpty)
{
$message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} cannot be blank.');
$this->addError($object, $attribute, $message);
switch ($file->getError()) {
case UPLOAD_ERR_OK:
if ($this->maxSize !== null && $file->getSize() > $this->maxSize) {
$this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit()));
}
if ($this->minSize !== null && $file->getSize() < $this->minSize) {
$this->addError($object, $attribute, $this->tooSmall, array('{file}' => $file->getName(), '{limit}' => $this->minSize));
}
if (!empty($this->types) && !in_array(strtolower(FileHelper::getExtension($file->getName())), $this->types, true)) {
$this->addError($object, $attribute, $this->wrongType, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $this->types)));
}
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit()));
break;
case UPLOAD_ERR_PARTIAL:
$this->addError($object, $attribute, $this->message);
Yii::warning('File was only partially uploaded: ' . $file->getName(), __METHOD__);
break;
case UPLOAD_ERR_NO_TMP_DIR:
$this->addError($object, $attribute, $this->message);
Yii::warning('Missing the temporary folder to store the uploaded file: ' . $file->getName(), __METHOD__);
break;
case UPLOAD_ERR_CANT_WRITE:
$this->addError($object, $attribute, $this->message);
Yii::warning('Failed to write the uploaded file to disk: ', $file->getName(), __METHOD__);
break;
case UPLOAD_ERR_EXTENSION:
$this->addError($object, $attribute, $this->message);
Yii::warning('File upload was stopped by some PHP extension: ', $file->getName(), __METHOD__);
break;
default:
break;
}
}
/**
* Returns the maximum size allowed for uploaded files.
* This is determined based on three factors:
* <ul>
* <li>'upload_max_filesize' in php.ini</li>
* <li>'MAX_FILE_SIZE' hidden field</li>
* <li>{@link maxSize}</li>
* </ul>
*
* - 'upload_max_filesize' in php.ini
* - 'MAX_FILE_SIZE' hidden field
* - [[maxSize]]
*
* @return integer the size limit for uploaded files.
*/
@ -203,10 +218,12 @@ class CFileValidator extends Validator
{
$limit = ini_get('upload_max_filesize');
$limit = $this->sizeToBytes($limit);
if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit)
if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) {
$limit = $this->maxSize;
if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit)
$limit = $_POST['MAX_FILE_SIZE'];
}
if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) {
$limit = (int)$_POST['MAX_FILE_SIZE'];
}
return $limit;
}
@ -218,12 +235,18 @@ class CFileValidator extends Validator
*/
private function sizeToBytes($sizeStr)
{
switch (substr($sizeStr, -1))
{
case 'M': case 'm': return (int)$sizeStr * 1048576;
case 'K': case 'k': return (int)$sizeStr * 1024;
case 'G': case 'g': return (int)$sizeStr * 1073741824;
default: return (int)$sizeStr;
switch (substr($sizeStr, -1)) {
case 'M':
case 'm':
return (int)$sizeStr * 1048576;
case 'K':
case 'k':
return (int)$sizeStr * 1024;
case 'G':
case 'g':
return (int)$sizeStr * 1073741824;
default:
return (int)$sizeStr;
}
}
}

246
framework/web/UploadedFile.php

@ -0,0 +1,246 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\web;
use yii\widgets\ActiveForm;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class UploadedFile extends \yii\base\Object
{
private static $_files;
private $_name;
private $_tempName;
private $_type;
private $_size;
private $_error;
/**
* Constructor.
* Instead of using the constructor to create a new instance,
* you should normally call [[getInstance()]] or [[getInstances()]]
* to obtain new instances.
* @param string $name the original name of the file being uploaded
* @param string $tempName the path of the uploaded file on the server.
* @param string $type the MIME-type of the uploaded file (such as "image/gif").
* @param integer $size the actual size of the uploaded file in bytes
* @param integer $error the error code
*/
public function __construct($name, $tempName, $type, $size, $error)
{
$this->_name = $name;
$this->_tempName = $tempName;
$this->_type = $type;
$this->_size = $size;
$this->_error = $error;
}
/**
* String output.
* This is PHP magic method that returns string representation of an object.
* The implementation here returns the uploaded file's name.
* @return string the string representation of the object
*/
public function __toString()
{
return $this->_name;
}
/**
* Returns an uploaded file for the given model attribute.
* The file should be uploaded using [[ActiveForm::fileInput()]].
* @param \yii\base\Model $model the data model
* @param string $attribute the attribute name. The attribute name may contain array indexes.
* For example, '[1]file' for tabular file uploading; and 'file[1]' for an element in a file array.
* @return UploadedFile 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)
{
$name = ActiveForm::getInputName($model, $attribute);
return static::getInstanceByName($name);
}
/**
* Returns all uploaded files for the given model attribute.
* @param \yii\base\Model $model the data model
* @param string $attribute the attribute name. The attribute name may contain array indexes
* for tabular file uploading, e.g. '[1]file'.
* @return UploadedFile[] array of UploadedFile objects.
* Empty array is returned if no available file was found for the given attribute.
*/
public static function getInstances($model, $attribute)
{
$name = ActiveForm::getInputName($model, $attribute);
return static::getInstancesByName($name);
}
/**
* Returns an uploaded file according to the given file input name.
* The name can be a plain string or a string like an array element (e.g. 'Post[imageFile]', or 'Post[0][imageFile]').
* @param string $name the name of the file input field.
* @return UploadedFile the instance of the uploaded file.
* Null is returned if no file is uploaded for the specified name.
*/
public static function getInstanceByName($name)
{
$files = static::loadFiles();
return isset($files[$name]) ? $files[$name] : null;
}
/**
* Returns an array of uploaded files corresponding to the specified file input name.
* This is mainly used when multiple files were uploaded and saved as 'files[0]', 'files[1]',
* 'files[n]'..., and you can retrieve them all by passing 'files' as the name.
* @param string $name the name of the array of files
* @return UploadedFile[] 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 sub-arrays regardless how deeply nested they are.
*/
public static function getInstancesByName($name)
{
$files = static::loadFiles();
if (isset($files[$name])) {
return array($files[$name]);
}
$results = array();
foreach ($files as $key => $file) {
if (strpos($key, "{$name}[") === 0) {
$results[] = self::$_files[$key];
}
}
return $results;
}
/**
* Cleans up the loaded UploadedFile instances.
* This method is mainly used by test scripts to set up a fixture.
*/
public static function reset()
{
self::$_files = null;
}
/**
* Saves the uploaded file.
* Note that this method uses php's move_uploaded_file() method. If the target file `$file`
* already exists, it will be overwritten.
* @param string $file the file path used to save the uploaded file
* @param boolean $deleteTempFile whether to delete the temporary file after saving.
* If true, you will not be able to save the uploaded file again in the current request.
* @return boolean true whether the file is saved successfully
* @see error
*/
public function saveAs($file, $deleteTempFile = true)
{
if ($this->_error == UPLOAD_ERR_OK) {
if ($deleteTempFile) {
return move_uploaded_file($this->_tempName, $file);
} elseif (is_uploaded_file($this->_tempName)) {
return copy($this->_tempName, $file);
}
}
return false;
}
/**
* @return string the original name of the file being uploaded
*/
public function getName()
{
return $this->_name;
}
/**
* @return string the path of the uploaded file on the server.
* Note, this is a temporary file which will be automatically deleted by PHP
* after the current request is processed.
*/
public function getTempName()
{
return $this->_tempName;
}
/**
* @return string the MIME-type of the uploaded file (such as "image/gif").
* Since this MIME type is not checked on the server side, do not take this value for granted.
* Instead, use [[FileHelper::getMimeType()]] to determine the exact MIME type.
*/
public function getType()
{
return $this->_type;
}
/**
* @return integer the actual size of the uploaded file in bytes
*/
public function getSize()
{
return $this->_size;
}
/**
* Returns an error code describing the status of this file uploading.
* @return integer the error code
* @see http://www.php.net/manual/en/features.file-upload.errors.php
*/
public function getError()
{
return $this->_error;
}
/**
* @return boolean whether there is an error with the uploaded file.
* Check [[error]] for detailed error code information.
*/
public function getHasError()
{
return $this->_error != UPLOAD_ERR_OK;
}
/**
* Creates UploadedFile instances from $_FILE.
* @return array the UploadedFile instances
*/
private static function loadFiles()
{
if (self::$_files === null) {
self::$_files = array();
if (isset($_FILES) && is_array($_FILES)) {
foreach ($_FILES as $class => $info) {
self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']);
}
}
}
return self::$_files;
}
/**
* Creates UploadedFile instances from $_FILE recursively.
* @param string $key key for identifying uploaded file: class name and sub-array indexes
* @param mixed $names file names provided by PHP
* @param mixed $tempNames temporary file names provided by PHP
* @param mixed $types file types provided by PHP
* @param mixed $sizes file sizes provided by PHP
* @param mixed $errors uploading issues provided by PHP
*/
private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors)
{
if (is_array($names)) {
foreach ($names as $i => $name) {
self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]);
}
} else {
self::$_files[$key] = new self($names, $tempNames, $types, $sizes, $errors);
}
}
}

63
framework/widgets/ActiveForm.php

@ -52,10 +52,6 @@ class ActiveForm extends Widget
public $enableClientValidation = false;
public $options = array();
/**
* @var array model-class mapped to name prefix
*/
public $modelMap;
/**
* @param Model|Model[] $models
@ -240,35 +236,6 @@ class ActiveForm extends Widget
return Html::radioList($name, $checked, $items, $options);
}
public function getInputName($model, $attribute)
{
$class = get_class($model);
if (isset($this->modelMap[$class])) {
$class = $this->modelMap[$class];
} elseif (($pos = strrpos($class, '\\')) !== false) {
$class = substr($class, $pos + 1);
}
if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) {
throw new InvalidParamException('Attribute name must contain word characters only.');
}
$prefix = $matches[1];
$attribute = $matches[2];
$suffix = $matches[3];
if ($class === '' && $prefix === '') {
return $attribute . $suffix;
} elseif ($class !== '') {
return $class . $prefix . "[$attribute]" . $suffix;
} else {
throw new InvalidParamException('Model name cannot be mapped to empty for tabular inputs.');
}
}
public function getInputId($model, $attribute)
{
$name = $this->getInputName($model, $attribute);
return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name);
}
public function getAttributeValue($model, $attribute)
{
if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) {
@ -299,4 +266,34 @@ class ActiveForm extends Widget
throw new InvalidParamException('Attribute name must contain word characters only.');
}
}
/**
* @param Model $model
* @param string $attribute
* @return string
* @throws \yii\base\InvalidParamException
*/
public static function getInputName($model, $attribute)
{
$formName = $model->formName();
if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) {
throw new InvalidParamException('Attribute name must contain word characters only.');
}
$prefix = $matches[1];
$attribute = $matches[2];
$suffix = $matches[3];
if ($formName === '' && $prefix === '') {
return $attribute . $suffix;
} elseif ($formName !== '') {
return $formName . $prefix . "[$attribute]" . $suffix;
} else {
throw new InvalidParamException(get_class($model) . '::formName() cannot be empty for tabular inputs.');
}
}
public static function getInputId($model, $attribute)
{
$name = static::getInputName($model, $attribute);
return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name);
}
}

Loading…
Cancel
Save