Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Validator\Constraints; Chris@0: Chris@0: use Symfony\Component\HttpFoundation\File\File as FileObject; Chris@0: use Symfony\Component\HttpFoundation\File\UploadedFile; Chris@0: use Symfony\Component\Validator\Constraint; Chris@0: use Symfony\Component\Validator\ConstraintValidator; Chris@0: use Symfony\Component\Validator\Exception\UnexpectedTypeException; Chris@0: Chris@0: /** Chris@0: * @author Bernhard Schussek Chris@0: */ Chris@0: class FileValidator extends ConstraintValidator Chris@0: { Chris@0: const KB_BYTES = 1000; Chris@0: const MB_BYTES = 1000000; Chris@0: const KIB_BYTES = 1024; Chris@0: const MIB_BYTES = 1048576; Chris@0: Chris@17: private static $suffices = [ Chris@0: 1 => 'bytes', Chris@0: self::KB_BYTES => 'kB', Chris@0: self::MB_BYTES => 'MB', Chris@0: self::KIB_BYTES => 'KiB', Chris@0: self::MIB_BYTES => 'MiB', Chris@17: ]; Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function validate($value, Constraint $constraint) Chris@0: { Chris@0: if (!$constraint instanceof File) { Chris@0: throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\File'); Chris@0: } Chris@0: Chris@0: if (null === $value || '' === $value) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if ($value instanceof UploadedFile && !$value->isValid()) { Chris@0: switch ($value->getError()) { Chris@0: case UPLOAD_ERR_INI_SIZE: Chris@0: $iniLimitSize = UploadedFile::getMaxFilesize(); Chris@0: if ($constraint->maxSize && $constraint->maxSize < $iniLimitSize) { Chris@0: $limitInBytes = $constraint->maxSize; Chris@0: $binaryFormat = $constraint->binaryFormat; Chris@0: } else { Chris@0: $limitInBytes = $iniLimitSize; Chris@17: $binaryFormat = null === $constraint->binaryFormat ? true : $constraint->binaryFormat; Chris@0: } Chris@0: Chris@0: list($sizeAsString, $limitAsString, $suffix) = $this->factorizeSizes(0, $limitInBytes, $binaryFormat); Chris@0: $this->context->buildViolation($constraint->uploadIniSizeErrorMessage) Chris@0: ->setParameter('{{ limit }}', $limitAsString) Chris@0: ->setParameter('{{ suffix }}', $suffix) Chris@0: ->setCode(UPLOAD_ERR_INI_SIZE) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: case UPLOAD_ERR_FORM_SIZE: Chris@0: $this->context->buildViolation($constraint->uploadFormSizeErrorMessage) Chris@0: ->setCode(UPLOAD_ERR_FORM_SIZE) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: case UPLOAD_ERR_PARTIAL: Chris@0: $this->context->buildViolation($constraint->uploadPartialErrorMessage) Chris@0: ->setCode(UPLOAD_ERR_PARTIAL) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: case UPLOAD_ERR_NO_FILE: Chris@0: $this->context->buildViolation($constraint->uploadNoFileErrorMessage) Chris@0: ->setCode(UPLOAD_ERR_NO_FILE) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: case UPLOAD_ERR_NO_TMP_DIR: Chris@0: $this->context->buildViolation($constraint->uploadNoTmpDirErrorMessage) Chris@0: ->setCode(UPLOAD_ERR_NO_TMP_DIR) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: case UPLOAD_ERR_CANT_WRITE: Chris@0: $this->context->buildViolation($constraint->uploadCantWriteErrorMessage) Chris@0: ->setCode(UPLOAD_ERR_CANT_WRITE) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: case UPLOAD_ERR_EXTENSION: Chris@0: $this->context->buildViolation($constraint->uploadExtensionErrorMessage) Chris@0: ->setCode(UPLOAD_ERR_EXTENSION) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: default: Chris@0: $this->context->buildViolation($constraint->uploadErrorMessage) Chris@0: ->setCode($value->getError()) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: } Chris@0: } Chris@0: Chris@17: if (!is_scalar($value) && !$value instanceof FileObject && !(\is_object($value) && method_exists($value, '__toString'))) { Chris@0: throw new UnexpectedTypeException($value, 'string'); Chris@0: } Chris@0: Chris@0: $path = $value instanceof FileObject ? $value->getPathname() : (string) $value; Chris@0: Chris@0: if (!is_file($path)) { Chris@0: $this->context->buildViolation($constraint->notFoundMessage) Chris@0: ->setParameter('{{ file }}', $this->formatValue($path)) Chris@0: ->setCode(File::NOT_FOUND_ERROR) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: if (!is_readable($path)) { Chris@0: $this->context->buildViolation($constraint->notReadableMessage) Chris@0: ->setParameter('{{ file }}', $this->formatValue($path)) Chris@0: ->setCode(File::NOT_READABLE_ERROR) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: $sizeInBytes = filesize($path); Chris@0: Chris@0: if (0 === $sizeInBytes) { Chris@0: $this->context->buildViolation($constraint->disallowEmptyMessage) Chris@0: ->setParameter('{{ file }}', $this->formatValue($path)) Chris@0: ->setCode(File::EMPTY_ERROR) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: } Chris@0: Chris@0: if ($constraint->maxSize) { Chris@0: $limitInBytes = $constraint->maxSize; Chris@0: Chris@0: if ($sizeInBytes > $limitInBytes) { Chris@0: list($sizeAsString, $limitAsString, $suffix) = $this->factorizeSizes($sizeInBytes, $limitInBytes, $constraint->binaryFormat); Chris@0: $this->context->buildViolation($constraint->maxSizeMessage) Chris@0: ->setParameter('{{ file }}', $this->formatValue($path)) Chris@0: ->setParameter('{{ size }}', $sizeAsString) Chris@0: ->setParameter('{{ limit }}', $limitAsString) Chris@0: ->setParameter('{{ suffix }}', $suffix) Chris@0: ->setCode(File::TOO_LARGE_ERROR) Chris@0: ->addViolation(); Chris@0: Chris@0: return; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($constraint->mimeTypes) { Chris@0: if (!$value instanceof FileObject) { Chris@0: $value = new FileObject($value); Chris@0: } Chris@0: Chris@0: $mimeTypes = (array) $constraint->mimeTypes; Chris@0: $mime = $value->getMimeType(); Chris@0: Chris@0: foreach ($mimeTypes as $mimeType) { Chris@0: if ($mimeType === $mime) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if ($discrete = strstr($mimeType, '/*', true)) { Chris@0: if (strstr($mime, '/', true) === $discrete) { Chris@0: return; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $this->context->buildViolation($constraint->mimeTypesMessage) Chris@0: ->setParameter('{{ file }}', $this->formatValue($path)) Chris@0: ->setParameter('{{ type }}', $this->formatValue($mime)) Chris@0: ->setParameter('{{ types }}', $this->formatValues($mimeTypes)) Chris@0: ->setCode(File::INVALID_MIME_TYPE_ERROR) Chris@0: ->addViolation(); Chris@0: } Chris@0: } Chris@0: Chris@0: private static function moreDecimalsThan($double, $numberOfDecimals) Chris@0: { Chris@17: return \strlen((string) $double) > \strlen(round($double, $numberOfDecimals)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Convert the limit to the smallest possible number Chris@0: * (i.e. try "MB", then "kB", then "bytes"). Chris@0: */ Chris@0: private function factorizeSizes($size, $limit, $binaryFormat) Chris@0: { Chris@0: if ($binaryFormat) { Chris@0: $coef = self::MIB_BYTES; Chris@0: $coefFactor = self::KIB_BYTES; Chris@0: } else { Chris@0: $coef = self::MB_BYTES; Chris@0: $coefFactor = self::KB_BYTES; Chris@0: } Chris@0: Chris@0: $limitAsString = (string) ($limit / $coef); Chris@0: Chris@0: // Restrict the limit to 2 decimals (without rounding! we Chris@0: // need the precise value) Chris@0: while (self::moreDecimalsThan($limitAsString, 2)) { Chris@0: $coef /= $coefFactor; Chris@0: $limitAsString = (string) ($limit / $coef); Chris@0: } Chris@0: Chris@0: // Convert size to the same measure, but round to 2 decimals Chris@0: $sizeAsString = (string) round($size / $coef, 2); Chris@0: Chris@0: // If the size and limit produce the same string output Chris@0: // (due to rounding), reduce the coefficient Chris@0: while ($sizeAsString === $limitAsString) { Chris@0: $coef /= $coefFactor; Chris@0: $limitAsString = (string) ($limit / $coef); Chris@0: $sizeAsString = (string) round($size / $coef, 2); Chris@0: } Chris@0: Chris@17: return [$sizeAsString, $limitAsString, self::$suffices[$coef]]; Chris@0: } Chris@0: }