1: <?php
2: /**
3: * CFileValidator class file.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11: /**
12: * CFileValidator verifies if an attribute is receiving a valid uploaded file.
13: *
14: * It uses the model class and attribute name to retrieve the information
15: * about the uploaded file. It then checks if a file is uploaded successfully,
16: * if the file size is within the limit and if the file type is allowed.
17: *
18: * This validator will attempt to fetch uploaded data if attribute is not
19: * previously set. Please note that this cannot be done if input is tabular:
20: * <pre>
21: * foreach($models as $i=>$model)
22: * $model->attribute = CUploadedFile::getInstance($model, "[$i]attribute");
23: * </pre>
24: * Please note that you must use {@link CUploadedFile::getInstances} for multiple
25: * file uploads.
26: *
27: * When using CFileValidator with an active record, the following code is often used:
28: * <pre>
29: * $model->attribute = CUploadedFile::getInstance($model, "attribute");
30: * if($model->save())
31: * {
32: * // single upload
33: * $model->attribute->saveAs($path);
34: * // multiple upload
35: * foreach($model->attribute as $file)
36: * $file->saveAs($path);
37: * }
38: * </pre>
39: *
40: * You can use {@link CFileValidator} to validate the file attribute.
41: *
42: * In addition to the {@link message} property for setting a custom error message,
43: * CFileValidator has a few custom error messages you can set that correspond to different
44: * validation scenarios. When the file is too large, you may use the {@link tooLarge} property
45: * to define a custom error message. Similarly for {@link tooSmall}, {@link wrongType} and
46: * {@link tooMany}. The messages may contain additional placeholders that will be replaced
47: * with the actual content. In addition to the "{attribute}" placeholder, recognized by all
48: * validators (see {@link CValidator}), CFileValidator allows for the following placeholders
49: * to be specified:
50: * <ul>
51: * <li>{file}: replaced with the name of the file.</li>
52: * <li>{limit}: when using {@link tooLarge}, replaced with {@link maxSize};
53: * when using {@link tooSmall}, replaced with {@link minSize}; and when using {@link tooMany}
54: * replaced with {@link maxFiles}.</li>
55: * <li>{extensions}: when using {@link wrongType}, it will be replaced with the allowed extensions.</li>
56: * </ul>
57: *
58: * @author Qiang Xue <qiang.xue@gmail.com>
59: * @package system.validators
60: * @since 1.0
61: */
62: class CFileValidator extends CValidator
63: {
64: /**
65: * @var boolean whether the attribute requires a file to be uploaded or not.
66: * Defaults to false, meaning a file is required to be uploaded.
67: * When no file is uploaded, the owner attribute is set to null to prevent
68: * setting arbitrary values.
69: */
70: public $allowEmpty=false;
71: /**
72: * @var mixed a list of file name extensions that are allowed to be uploaded.
73: * This can be either an array or a string consisting of file extension names
74: * separated by space or comma (e.g. "gif, jpg").
75: * Extension names are case-insensitive. Defaults to null, meaning all file name
76: * extensions are allowed.
77: */
78: public $types;
79: /**
80: * @var mixed a list of MIME-types of the file that are allowed to be uploaded.
81: * This can be either an array or a string consisting of MIME-types separated
82: * by space or comma (e.g. "image/gif, image/jpeg"). MIME-types are
83: * case-insensitive. Defaults to null, meaning all MIME-types are allowed.
84: * In order to use this property fileinfo PECL extension should be installed.
85: * @since 1.1.11
86: */
87: public $mimeTypes;
88: /**
89: * @var integer the minimum number of bytes required for the uploaded file.
90: * Defaults to null, meaning no limit.
91: * @see tooSmall
92: */
93: public $minSize;
94: /**
95: * @var integer the maximum number of bytes required for the uploaded file.
96: * Defaults to null, meaning no limit.
97: * Note, the size limit is also affected by 'upload_max_filesize' INI setting
98: * and the 'MAX_FILE_SIZE' hidden field value.
99: * @see tooLarge
100: */
101: public $maxSize;
102: /**
103: * @var string the error message used when the uploaded file is too large.
104: * @see maxSize
105: */
106: public $tooLarge;
107: /**
108: * @var string the error message used when the uploaded file is too small.
109: * @see minSize
110: */
111: public $tooSmall;
112: /**
113: * @var string the error message used when the uploaded file has an extension name
114: * that is not listed among {@link types}.
115: */
116: public $wrongType;
117: /**
118: * @var string the error message used when the uploaded file has a MIME-type
119: * that is not listed among {@link mimeTypes}. In order to use this property
120: * fileinfo PECL extension should be installed.
121: * @since 1.1.11
122: */
123: public $wrongMimeType;
124: /**
125: * @var integer the maximum file count the given attribute can hold.
126: * It defaults to 1, meaning single file upload. By defining a higher number,
127: * multiple uploads become possible.
128: */
129: public $maxFiles=1;
130: /**
131: * @var string the error message used if the count of multiple uploads exceeds
132: * limit.
133: */
134: public $tooMany;
135:
136: /**
137: * Set the attribute and then validates using {@link validateFile}.
138: * If there is any error, the error message is added to the object.
139: * @param CModel $object the object being validated
140: * @param string $attribute the attribute being validated
141: */
142: protected function validateAttribute($object, $attribute)
143: {
144: $files=$object->$attribute;
145: if($this->maxFiles > 1)
146: {
147: if(!is_array($files) || !isset($files[0]) || !$files[0] instanceof CUploadedFile)
148: $files = CUploadedFile::getInstances($object, $attribute);
149: if(array()===$files)
150: return $this->emptyAttribute($object, $attribute);
151: if(count($files) > $this->maxFiles)
152: {
153: $message=$this->tooMany!==null?$this->tooMany : Yii::t('yii', '{attribute} cannot accept more than {limit} files.');
154: $this->addError($object, $attribute, $message, array('{attribute}'=>$attribute, '{limit}'=>$this->maxFiles));
155: }
156: else
157: foreach($files as $file)
158: $this->validateFile($object, $attribute, $file);
159: }
160: else
161: {
162: if (is_array($files))
163: {
164: if (count($files) > 1)
165: {
166: $message=$this->tooMany!==null?$this->tooMany : Yii::t('yii', '{attribute} cannot accept more than {limit} files.');
167: $this->addError($object, $attribute, $message, array('{attribute}'=>$attribute, '{limit}'=>$this->maxFiles));
168: return;
169: }
170: else
171: $file = empty($files) ? null : reset($files);
172: }
173: else
174: $file = $files;
175: if(!$file instanceof CUploadedFile)
176: {
177: $file = CUploadedFile::getInstance($object, $attribute);
178: if(null===$file)
179: return $this->emptyAttribute($object, $attribute);
180: }
181: $this->validateFile($object, $attribute, $file);
182: }
183: }
184:
185: /**
186: * Internally validates a file object.
187: * @param CModel $object the object being validated
188: * @param string $attribute the attribute being validated
189: * @param CUploadedFile $file uploaded file passed to check against a set of rules
190: * @throws CException if failed to upload the file
191: */
192: protected function validateFile($object, $attribute, $file)
193: {
194: $error=(null===$file ? null : $file->getError());
195: if($error==UPLOAD_ERR_INI_SIZE || $error==UPLOAD_ERR_FORM_SIZE || $this->maxSize!==null && $file->getSize()>$this->maxSize)
196: {
197: $message=$this->tooLarge!==null?$this->tooLarge : Yii::t('yii','The file "{file}" is too large. Its size cannot exceed {limit} bytes.');
198: $this->addError($object,$attribute,$message,array('{file}'=>CHtml::encode($file->getName()), '{limit}'=>$this->getSizeLimit()));
199: if($error!==UPLOAD_ERR_OK)
200: return;
201: }
202: elseif($error!==UPLOAD_ERR_OK)
203: {
204: if($error==UPLOAD_ERR_NO_FILE)
205: return $this->emptyAttribute($object, $attribute);
206: elseif($error==UPLOAD_ERR_PARTIAL)
207: throw new CException(Yii::t('yii','The file "{file}" was only partially uploaded.',array('{file}'=>CHtml::encode($file->getName()))));
208: elseif($error==UPLOAD_ERR_NO_TMP_DIR)
209: throw new CException(Yii::t('yii','Missing the temporary folder to store the uploaded file "{file}".',array('{file}'=>CHtml::encode($file->getName()))));
210: elseif($error==UPLOAD_ERR_CANT_WRITE)
211: throw new CException(Yii::t('yii','Failed to write the uploaded file "{file}" to disk.',array('{file}'=>CHtml::encode($file->getName()))));
212: elseif(defined('UPLOAD_ERR_EXTENSION') && $error==UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above
213: throw new CException(Yii::t('yii','A PHP extension stopped the file upload.'));
214: else
215: throw new CException(Yii::t('yii','Unable to upload the file "{file}" because of an unrecognized error.',array('{file}'=>CHtml::encode($file->getName()))));
216: }
217:
218: if($this->minSize!==null && $file->getSize()<$this->minSize)
219: {
220: $message=$this->tooSmall!==null?$this->tooSmall : Yii::t('yii','The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.');
221: $this->addError($object,$attribute,$message,array('{file}'=>CHtml::encode($file->getName()), '{limit}'=>$this->minSize));
222: }
223:
224: if($this->types!==null)
225: {
226: if(is_string($this->types))
227: $types=preg_split('/[\s,]+/',strtolower($this->types),-1,PREG_SPLIT_NO_EMPTY);
228: else
229: $types=$this->types;
230: if(!in_array(strtolower($file->getExtensionName()),$types))
231: {
232: $message=$this->wrongType!==null?$this->wrongType : Yii::t('yii','The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.');
233: $this->addError($object,$attribute,$message,array('{file}'=>CHtml::encode($file->getName()), '{extensions}'=>implode(', ',$types)));
234: }
235: }
236:
237: if($this->mimeTypes!==null && !empty($file->tempName))
238: {
239: if(function_exists('finfo_open'))
240: {
241: $mimeType=false;
242: if($info=finfo_open(defined('FILEINFO_MIME_TYPE') ? FILEINFO_MIME_TYPE : FILEINFO_MIME))
243: $mimeType=finfo_file($info,$file->getTempName());
244: }
245: elseif(function_exists('mime_content_type'))
246: $mimeType=mime_content_type($file->getTempName());
247: else
248: throw new CException(Yii::t('yii','In order to use MIME-type validation provided by CFileValidator fileinfo PECL extension should be installed.'));
249:
250: if(is_string($this->mimeTypes))
251: $mimeTypes=preg_split('/[\s,]+/',strtolower($this->mimeTypes),-1,PREG_SPLIT_NO_EMPTY);
252: else
253: $mimeTypes=$this->mimeTypes;
254:
255: if($mimeType===false || !in_array(strtolower($mimeType),$mimeTypes))
256: {
257: $message=$this->wrongMimeType!==null?$this->wrongMimeType : Yii::t('yii','The file "{file}" cannot be uploaded. Only files of these MIME-types are allowed: {mimeTypes}.');
258: $this->addError($object,$attribute,$message,array('{file}'=>CHtml::encode($file->getName()), '{mimeTypes}'=>implode(', ',$mimeTypes)));
259: }
260: }
261: }
262:
263: /**
264: * Raises an error to inform end user about blank attribute.
265: * Sets the owner attribute to null to prevent setting arbitrary values.
266: * @param CModel $object the object being validated
267: * @param string $attribute the attribute being validated
268: */
269: protected function emptyAttribute($object, $attribute)
270: {
271: if($this->safe)
272: $object->$attribute=null;
273:
274: if(!$this->allowEmpty)
275: {
276: $message=$this->message!==null?$this->message : Yii::t('yii','{attribute} cannot be blank.');
277: $this->addError($object,$attribute,$message);
278: }
279: }
280:
281: /**
282: * Returns the maximum size allowed for uploaded files.
283: * This is determined based on three factors:
284: * <ul>
285: * <li>'upload_max_filesize' in php.ini</li>
286: * <li>'MAX_FILE_SIZE' hidden field</li>
287: * <li>{@link maxSize}</li>
288: * </ul>
289: *
290: * @return integer the size limit for uploaded files.
291: */
292: protected function getSizeLimit()
293: {
294: $limit=ini_get('upload_max_filesize');
295: $limit=$this->sizeToBytes($limit);
296: if($this->maxSize!==null && $limit>0 && $this->maxSize<$limit)
297: $limit=$this->maxSize;
298: if(isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE']>0 && $_POST['MAX_FILE_SIZE']<$limit)
299: $limit=$_POST['MAX_FILE_SIZE'];
300: return $limit;
301: }
302:
303: /**
304: * Converts php.ini style size to bytes. Examples of size strings are: 150, 1g, 500k, 5M (size suffix
305: * is case insensitive). If you pass here the number with a fractional part, then everything after
306: * the decimal point will be ignored (php.ini values common behavior). For example 1.5G value would be
307: * treated as 1G and 1073741824 number will be returned as a result. This method is public
308: * (was private before) since 1.1.11.
309: *
310: * @param string $sizeStr the size string to convert.
311: * @return integer the byte count in the given size string.
312: * @since 1.1.11
313: */
314: public function sizeToBytes($sizeStr)
315: {
316: // get the latest character
317: switch (strtolower(substr($sizeStr, -1)))
318: {
319: case 'm': return (int)$sizeStr * 1048576; // 1024 * 1024
320: case 'k': return (int)$sizeStr * 1024; // 1024
321: case 'g': return (int)$sizeStr * 1073741824; // 1024 * 1024 * 1024
322: default: return (int)$sizeStr; // do nothing
323: }
324: }
325: }
326: