FileValidator.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Validator\Constraints;
  11. use Symfony\Component\HttpFoundation\File\File as FileObject;
  12. use Symfony\Component\HttpFoundation\File\UploadedFile;
  13. use Symfony\Component\Mime\MimeTypes;
  14. use Symfony\Component\Validator\Constraint;
  15. use Symfony\Component\Validator\ConstraintValidator;
  16. use Symfony\Component\Validator\Exception\LogicException;
  17. use Symfony\Component\Validator\Exception\UnexpectedTypeException;
  18. use Symfony\Component\Validator\Exception\UnexpectedValueException;
  19. /**
  20. * @author Bernhard Schussek <bschussek@gmail.com>
  21. */
  22. class FileValidator extends ConstraintValidator
  23. {
  24. public const KB_BYTES = 1000;
  25. public const MB_BYTES = 1000000;
  26. public const KIB_BYTES = 1024;
  27. public const MIB_BYTES = 1048576;
  28. private const SUFFICES = [
  29. 1 => 'bytes',
  30. self::KB_BYTES => 'kB',
  31. self::MB_BYTES => 'MB',
  32. self::KIB_BYTES => 'KiB',
  33. self::MIB_BYTES => 'MiB',
  34. ];
  35. /**
  36. * {@inheritdoc}
  37. */
  38. public function validate($value, Constraint $constraint)
  39. {
  40. if (!$constraint instanceof File) {
  41. throw new UnexpectedTypeException($constraint, File::class);
  42. }
  43. if (null === $value || '' === $value) {
  44. return;
  45. }
  46. if ($value instanceof UploadedFile && !$value->isValid()) {
  47. switch ($value->getError()) {
  48. case \UPLOAD_ERR_INI_SIZE:
  49. $iniLimitSize = UploadedFile::getMaxFilesize();
  50. if ($constraint->maxSize && $constraint->maxSize < $iniLimitSize) {
  51. $limitInBytes = $constraint->maxSize;
  52. $binaryFormat = $constraint->binaryFormat;
  53. } else {
  54. $limitInBytes = $iniLimitSize;
  55. $binaryFormat = null === $constraint->binaryFormat ? true : $constraint->binaryFormat;
  56. }
  57. [, $limitAsString, $suffix] = $this->factorizeSizes(0, $limitInBytes, $binaryFormat);
  58. $this->context->buildViolation($constraint->uploadIniSizeErrorMessage)
  59. ->setParameter('{{ limit }}', $limitAsString)
  60. ->setParameter('{{ suffix }}', $suffix)
  61. ->setCode((string) \UPLOAD_ERR_INI_SIZE)
  62. ->addViolation();
  63. return;
  64. case \UPLOAD_ERR_FORM_SIZE:
  65. $this->context->buildViolation($constraint->uploadFormSizeErrorMessage)
  66. ->setCode((string) \UPLOAD_ERR_FORM_SIZE)
  67. ->addViolation();
  68. return;
  69. case \UPLOAD_ERR_PARTIAL:
  70. $this->context->buildViolation($constraint->uploadPartialErrorMessage)
  71. ->setCode((string) \UPLOAD_ERR_PARTIAL)
  72. ->addViolation();
  73. return;
  74. case \UPLOAD_ERR_NO_FILE:
  75. $this->context->buildViolation($constraint->uploadNoFileErrorMessage)
  76. ->setCode((string) \UPLOAD_ERR_NO_FILE)
  77. ->addViolation();
  78. return;
  79. case \UPLOAD_ERR_NO_TMP_DIR:
  80. $this->context->buildViolation($constraint->uploadNoTmpDirErrorMessage)
  81. ->setCode((string) \UPLOAD_ERR_NO_TMP_DIR)
  82. ->addViolation();
  83. return;
  84. case \UPLOAD_ERR_CANT_WRITE:
  85. $this->context->buildViolation($constraint->uploadCantWriteErrorMessage)
  86. ->setCode((string) \UPLOAD_ERR_CANT_WRITE)
  87. ->addViolation();
  88. return;
  89. case \UPLOAD_ERR_EXTENSION:
  90. $this->context->buildViolation($constraint->uploadExtensionErrorMessage)
  91. ->setCode((string) \UPLOAD_ERR_EXTENSION)
  92. ->addViolation();
  93. return;
  94. default:
  95. $this->context->buildViolation($constraint->uploadErrorMessage)
  96. ->setCode((string) $value->getError())
  97. ->addViolation();
  98. return;
  99. }
  100. }
  101. if (!is_scalar($value) && !$value instanceof FileObject && !(\is_object($value) && method_exists($value, '__toString'))) {
  102. throw new UnexpectedValueException($value, 'string');
  103. }
  104. $path = $value instanceof FileObject ? $value->getPathname() : (string) $value;
  105. if (!is_file($path)) {
  106. $this->context->buildViolation($constraint->notFoundMessage)
  107. ->setParameter('{{ file }}', $this->formatValue($path))
  108. ->setCode(File::NOT_FOUND_ERROR)
  109. ->addViolation();
  110. return;
  111. }
  112. if (!is_readable($path)) {
  113. $this->context->buildViolation($constraint->notReadableMessage)
  114. ->setParameter('{{ file }}', $this->formatValue($path))
  115. ->setCode(File::NOT_READABLE_ERROR)
  116. ->addViolation();
  117. return;
  118. }
  119. $sizeInBytes = filesize($path);
  120. $basename = $value instanceof UploadedFile ? $value->getClientOriginalName() : basename($path);
  121. if (0 === $sizeInBytes) {
  122. $this->context->buildViolation($constraint->disallowEmptyMessage)
  123. ->setParameter('{{ file }}', $this->formatValue($path))
  124. ->setParameter('{{ name }}', $this->formatValue($basename))
  125. ->setCode(File::EMPTY_ERROR)
  126. ->addViolation();
  127. return;
  128. }
  129. if ($constraint->maxSize) {
  130. $limitInBytes = $constraint->maxSize;
  131. if ($sizeInBytes > $limitInBytes) {
  132. [$sizeAsString, $limitAsString, $suffix] = $this->factorizeSizes($sizeInBytes, $limitInBytes, $constraint->binaryFormat);
  133. $this->context->buildViolation($constraint->maxSizeMessage)
  134. ->setParameter('{{ file }}', $this->formatValue($path))
  135. ->setParameter('{{ size }}', $sizeAsString)
  136. ->setParameter('{{ limit }}', $limitAsString)
  137. ->setParameter('{{ suffix }}', $suffix)
  138. ->setParameter('{{ name }}', $this->formatValue($basename))
  139. ->setCode(File::TOO_LARGE_ERROR)
  140. ->addViolation();
  141. return;
  142. }
  143. }
  144. if ($constraint->mimeTypes) {
  145. if ($value instanceof FileObject) {
  146. $mime = $value->getMimeType();
  147. } elseif (class_exists(MimeTypes::class)) {
  148. $mime = MimeTypes::getDefault()->guessMimeType($path);
  149. } elseif (!class_exists(FileObject::class)) {
  150. throw new LogicException('You cannot validate the mime-type of files as the Mime component is not installed. Try running "composer require symfony/mime".');
  151. } else {
  152. $mime = (new FileObject($value))->getMimeType();
  153. }
  154. $mimeTypes = (array) $constraint->mimeTypes;
  155. foreach ($mimeTypes as $mimeType) {
  156. if ($mimeType === $mime) {
  157. return;
  158. }
  159. if ($discrete = strstr($mimeType, '/*', true)) {
  160. if (strstr($mime, '/', true) === $discrete) {
  161. return;
  162. }
  163. }
  164. }
  165. $this->context->buildViolation($constraint->mimeTypesMessage)
  166. ->setParameter('{{ file }}', $this->formatValue($path))
  167. ->setParameter('{{ type }}', $this->formatValue($mime))
  168. ->setParameter('{{ types }}', $this->formatValues($mimeTypes))
  169. ->setParameter('{{ name }}', $this->formatValue($basename))
  170. ->setCode(File::INVALID_MIME_TYPE_ERROR)
  171. ->addViolation();
  172. }
  173. }
  174. private static function moreDecimalsThan(string $double, int $numberOfDecimals): bool
  175. {
  176. return \strlen($double) > \strlen(round($double, $numberOfDecimals));
  177. }
  178. /**
  179. * Convert the limit to the smallest possible number
  180. * (i.e. try "MB", then "kB", then "bytes").
  181. *
  182. * @param int|float $limit
  183. */
  184. private function factorizeSizes(int $size, $limit, bool $binaryFormat): array
  185. {
  186. if ($binaryFormat) {
  187. $coef = self::MIB_BYTES;
  188. $coefFactor = self::KIB_BYTES;
  189. } else {
  190. $coef = self::MB_BYTES;
  191. $coefFactor = self::KB_BYTES;
  192. }
  193. $limitAsString = (string) ($limit / $coef);
  194. // Restrict the limit to 2 decimals (without rounding! we
  195. // need the precise value)
  196. while (self::moreDecimalsThan($limitAsString, 2)) {
  197. $coef /= $coefFactor;
  198. $limitAsString = (string) ($limit / $coef);
  199. }
  200. // Convert size to the same measure, but round to 2 decimals
  201. $sizeAsString = (string) round($size / $coef, 2);
  202. // If the size and limit produce the same string output
  203. // (due to rounding), reduce the coefficient
  204. while ($sizeAsString === $limitAsString) {
  205. $coef /= $coefFactor;
  206. $limitAsString = (string) ($limit / $coef);
  207. $sizeAsString = (string) round($size / $coef, 2);
  208. }
  209. return [$sizeAsString, $limitAsString, self::SUFFICES[$coef]];
  210. }
  211. }