4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Validator\Constraints;
14 use Symfony\Component\HttpFoundation\File\File as FileObject;
15 use Symfony\Component\HttpFoundation\File\UploadedFile;
16 use Symfony\Component\Validator\Context\ExecutionContextInterface;
17 use Symfony\Component\Validator\Constraint;
18 use Symfony\Component\Validator\ConstraintValidator;
19 use Symfony\Component\Validator\Exception\UnexpectedTypeException;
22 * @author Bernhard Schussek <bschussek@gmail.com>
24 class FileValidator extends ConstraintValidator
26 const KB_BYTES = 1000;
27 const MB_BYTES = 1000000;
28 const KIB_BYTES = 1024;
29 const MIB_BYTES = 1048576;
31 private static $suffices = array(
33 self::KB_BYTES => 'kB',
34 self::MB_BYTES => 'MB',
35 self::KIB_BYTES => 'KiB',
36 self::MIB_BYTES => 'MiB',
42 public function validate($value, Constraint $constraint)
44 if (!$constraint instanceof File) {
45 throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\File');
48 if (null === $value || '' === $value) {
52 if ($value instanceof UploadedFile && !$value->isValid()) {
53 switch ($value->getError()) {
54 case UPLOAD_ERR_INI_SIZE:
55 $iniLimitSize = UploadedFile::getMaxFilesize();
56 if ($constraint->maxSize && $constraint->maxSize < $iniLimitSize) {
57 $limitInBytes = $constraint->maxSize;
58 $binaryFormat = $constraint->binaryFormat;
60 $limitInBytes = $iniLimitSize;
64 list($sizeAsString, $limitAsString, $suffix) = $this->factorizeSizes(0, $limitInBytes, $binaryFormat);
65 if ($this->context instanceof ExecutionContextInterface) {
66 $this->context->buildViolation($constraint->uploadIniSizeErrorMessage)
67 ->setParameter('{{ limit }}', $limitAsString)
68 ->setParameter('{{ suffix }}', $suffix)
69 ->setCode(UPLOAD_ERR_INI_SIZE)
72 $this->buildViolation($constraint->uploadIniSizeErrorMessage)
73 ->setParameter('{{ limit }}', $limitAsString)
74 ->setParameter('{{ suffix }}', $suffix)
75 ->setCode(UPLOAD_ERR_INI_SIZE)
80 case UPLOAD_ERR_FORM_SIZE:
81 if ($this->context instanceof ExecutionContextInterface) {
82 $this->context->buildViolation($constraint->uploadFormSizeErrorMessage)
83 ->setCode(UPLOAD_ERR_FORM_SIZE)
86 $this->buildViolation($constraint->uploadFormSizeErrorMessage)
87 ->setCode(UPLOAD_ERR_FORM_SIZE)
92 case UPLOAD_ERR_PARTIAL:
93 if ($this->context instanceof ExecutionContextInterface) {
94 $this->context->buildViolation($constraint->uploadPartialErrorMessage)
95 ->setCode(UPLOAD_ERR_PARTIAL)
98 $this->buildViolation($constraint->uploadPartialErrorMessage)
99 ->setCode(UPLOAD_ERR_PARTIAL)
104 case UPLOAD_ERR_NO_FILE:
105 if ($this->context instanceof ExecutionContextInterface) {
106 $this->context->buildViolation($constraint->uploadNoFileErrorMessage)
107 ->setCode(UPLOAD_ERR_NO_FILE)
110 $this->buildViolation($constraint->uploadNoFileErrorMessage)
111 ->setCode(UPLOAD_ERR_NO_FILE)
116 case UPLOAD_ERR_NO_TMP_DIR:
117 if ($this->context instanceof ExecutionContextInterface) {
118 $this->context->buildViolation($constraint->uploadNoTmpDirErrorMessage)
119 ->setCode(UPLOAD_ERR_NO_TMP_DIR)
122 $this->buildViolation($constraint->uploadNoTmpDirErrorMessage)
123 ->setCode(UPLOAD_ERR_NO_TMP_DIR)
128 case UPLOAD_ERR_CANT_WRITE:
129 if ($this->context instanceof ExecutionContextInterface) {
130 $this->context->buildViolation($constraint->uploadCantWriteErrorMessage)
131 ->setCode(UPLOAD_ERR_CANT_WRITE)
134 $this->buildViolation($constraint->uploadCantWriteErrorMessage)
135 ->setCode(UPLOAD_ERR_CANT_WRITE)
140 case UPLOAD_ERR_EXTENSION:
141 if ($this->context instanceof ExecutionContextInterface) {
142 $this->context->buildViolation($constraint->uploadExtensionErrorMessage)
143 ->setCode(UPLOAD_ERR_EXTENSION)
146 $this->buildViolation($constraint->uploadExtensionErrorMessage)
147 ->setCode(UPLOAD_ERR_EXTENSION)
153 if ($this->context instanceof ExecutionContextInterface) {
154 $this->context->buildViolation($constraint->uploadErrorMessage)
155 ->setCode($value->getError())
158 $this->buildViolation($constraint->uploadErrorMessage)
159 ->setCode($value->getError())
167 if (!is_scalar($value) && !$value instanceof FileObject && !(is_object($value) && method_exists($value, '__toString'))) {
168 throw new UnexpectedTypeException($value, 'string');
171 $path = $value instanceof FileObject ? $value->getPathname() : (string) $value;
173 if (!is_file($path)) {
174 if ($this->context instanceof ExecutionContextInterface) {
175 $this->context->buildViolation($constraint->notFoundMessage)
176 ->setParameter('{{ file }}', $this->formatValue($path))
177 ->setCode(File::NOT_FOUND_ERROR)
180 $this->buildViolation($constraint->notFoundMessage)
181 ->setParameter('{{ file }}', $this->formatValue($path))
182 ->setCode(File::NOT_FOUND_ERROR)
189 if (!is_readable($path)) {
190 if ($this->context instanceof ExecutionContextInterface) {
191 $this->context->buildViolation($constraint->notReadableMessage)
192 ->setParameter('{{ file }}', $this->formatValue($path))
193 ->setCode(File::NOT_READABLE_ERROR)
196 $this->buildViolation($constraint->notReadableMessage)
197 ->setParameter('{{ file }}', $this->formatValue($path))
198 ->setCode(File::NOT_READABLE_ERROR)
205 $sizeInBytes = filesize($path);
207 if (0 === $sizeInBytes) {
208 if ($this->context instanceof ExecutionContextInterface) {
209 $this->context->buildViolation($constraint->disallowEmptyMessage)
210 ->setParameter('{{ file }}', $this->formatValue($path))
211 ->setCode(File::EMPTY_ERROR)
214 $this->buildViolation($constraint->disallowEmptyMessage)
215 ->setParameter('{{ file }}', $this->formatValue($path))
216 ->setCode(File::EMPTY_ERROR)
223 if ($constraint->maxSize) {
224 $limitInBytes = $constraint->maxSize;
226 if ($sizeInBytes > $limitInBytes) {
227 list($sizeAsString, $limitAsString, $suffix) = $this->factorizeSizes($sizeInBytes, $limitInBytes, $constraint->binaryFormat);
228 if ($this->context instanceof ExecutionContextInterface) {
229 $this->context->buildViolation($constraint->maxSizeMessage)
230 ->setParameter('{{ file }}', $this->formatValue($path))
231 ->setParameter('{{ size }}', $sizeAsString)
232 ->setParameter('{{ limit }}', $limitAsString)
233 ->setParameter('{{ suffix }}', $suffix)
234 ->setCode(File::TOO_LARGE_ERROR)
237 $this->buildViolation($constraint->maxSizeMessage)
238 ->setParameter('{{ file }}', $this->formatValue($path))
239 ->setParameter('{{ size }}', $sizeAsString)
240 ->setParameter('{{ limit }}', $limitAsString)
241 ->setParameter('{{ suffix }}', $suffix)
242 ->setCode(File::TOO_LARGE_ERROR)
250 if ($constraint->mimeTypes) {
251 if (!$value instanceof FileObject) {
252 $value = new FileObject($value);
255 $mimeTypes = (array) $constraint->mimeTypes;
256 $mime = $value->getMimeType();
258 foreach ($mimeTypes as $mimeType) {
259 if ($mimeType === $mime) {
263 if ($discrete = strstr($mimeType, '/*', true)) {
264 if (strstr($mime, '/', true) === $discrete) {
270 if ($this->context instanceof ExecutionContextInterface) {
271 $this->context->buildViolation($constraint->mimeTypesMessage)
272 ->setParameter('{{ file }}', $this->formatValue($path))
273 ->setParameter('{{ type }}', $this->formatValue($mime))
274 ->setParameter('{{ types }}', $this->formatValues($mimeTypes))
275 ->setCode(File::INVALID_MIME_TYPE_ERROR)
278 $this->buildViolation($constraint->mimeTypesMessage)
279 ->setParameter('{{ file }}', $this->formatValue($path))
280 ->setParameter('{{ type }}', $this->formatValue($mime))
281 ->setParameter('{{ types }}', $this->formatValues($mimeTypes))
282 ->setCode(File::INVALID_MIME_TYPE_ERROR)
288 private static function moreDecimalsThan($double, $numberOfDecimals)
290 return strlen((string) $double) > strlen(round($double, $numberOfDecimals));
294 * Convert the limit to the smallest possible number
295 * (i.e. try "MB", then "kB", then "bytes").
297 private function factorizeSizes($size, $limit, $binaryFormat)
300 $coef = self::MIB_BYTES;
301 $coefFactor = self::KIB_BYTES;
303 $coef = self::MB_BYTES;
304 $coefFactor = self::KB_BYTES;
307 $limitAsString = (string) ($limit / $coef);
309 // Restrict the limit to 2 decimals (without rounding! we
310 // need the precise value)
311 while (self::moreDecimalsThan($limitAsString, 2)) {
312 $coef /= $coefFactor;
313 $limitAsString = (string) ($limit / $coef);
316 // Convert size to the same measure, but round to 2 decimals
317 $sizeAsString = (string) round($size / $coef, 2);
319 // If the size and limit produce the same string output
320 // (due to rounding), reduce the coefficient
321 while ($sizeAsString === $limitAsString) {
322 $coef /= $coefFactor;
323 $limitAsString = (string) ($limit / $coef);
324 $sizeAsString = (string) round($size / $coef, 2);
327 return array($sizeAsString, $limitAsString, self::$suffices[$coef]);