vendor/symfony/form/Extension/Core/Type/FileType.php line 26

  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\Form\Extension\Core\Type;
  11. use Symfony\Component\Form\AbstractType;
  12. use Symfony\Component\Form\FileUploadError;
  13. use Symfony\Component\Form\FormBuilderInterface;
  14. use Symfony\Component\Form\FormEvent;
  15. use Symfony\Component\Form\FormEvents;
  16. use Symfony\Component\Form\FormInterface;
  17. use Symfony\Component\Form\FormView;
  18. use Symfony\Component\HttpFoundation\File\File;
  19. use Symfony\Component\OptionsResolver\Options;
  20. use Symfony\Component\OptionsResolver\OptionsResolver;
  21. use Symfony\Contracts\Translation\TranslatorInterface;
  22. class FileType extends AbstractType
  23. {
  24.     public const KIB_BYTES 1024;
  25.     public const MIB_BYTES 1048576;
  26.     private const SUFFIXES = [
  27.         => 'bytes',
  28.         self::KIB_BYTES => 'KiB',
  29.         self::MIB_BYTES => 'MiB',
  30.     ];
  31.     private ?TranslatorInterface $translator;
  32.     public function __construct(TranslatorInterface $translator null)
  33.     {
  34.         $this->translator $translator;
  35.     }
  36.     public function buildForm(FormBuilderInterface $builder, array $options)
  37.     {
  38.         // Ensure that submitted data is always an uploaded file or an array of some
  39.         $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
  40.             $form $event->getForm();
  41.             $requestHandler $form->getConfig()->getRequestHandler();
  42.             if ($options['multiple']) {
  43.                 $data = [];
  44.                 $files $event->getData();
  45.                 if (!\is_array($files)) {
  46.                     $files = [];
  47.                 }
  48.                 foreach ($files as $file) {
  49.                     if ($requestHandler->isFileUpload($file)) {
  50.                         $data[] = $file;
  51.                         if (method_exists($requestHandler'getUploadFileError') && null !== $errorCode $requestHandler->getUploadFileError($file)) {
  52.                             $form->addError($this->getFileUploadError($errorCode));
  53.                         }
  54.                     }
  55.                 }
  56.                 // Since the array is never considered empty in the view data format
  57.                 // on submission, we need to evaluate the configured empty data here
  58.                 if ([] === $data) {
  59.                     $emptyData $form->getConfig()->getEmptyData();
  60.                     $data $emptyData instanceof \Closure $emptyData($form$data) : $emptyData;
  61.                 }
  62.                 $event->setData($data);
  63.             } elseif ($requestHandler->isFileUpload($event->getData()) && method_exists($requestHandler'getUploadFileError') && null !== $errorCode $requestHandler->getUploadFileError($event->getData())) {
  64.                 $form->addError($this->getFileUploadError($errorCode));
  65.             } elseif (!$requestHandler->isFileUpload($event->getData())) {
  66.                 $event->setData(null);
  67.             }
  68.         });
  69.     }
  70.     public function buildView(FormView $viewFormInterface $form, array $options)
  71.     {
  72.         if ($options['multiple']) {
  73.             $view->vars['full_name'] .= '[]';
  74.             $view->vars['attr']['multiple'] = 'multiple';
  75.         }
  76.         $view->vars array_replace($view->vars, [
  77.             'type' => 'file',
  78.             'value' => '',
  79.         ]);
  80.     }
  81.     public function finishView(FormView $viewFormInterface $form, array $options)
  82.     {
  83.         $view->vars['multipart'] = true;
  84.     }
  85.     public function configureOptions(OptionsResolver $resolver)
  86.     {
  87.         $dataClass null;
  88.         if (class_exists(File::class)) {
  89.             $dataClass = function (Options $options) {
  90.                 return $options['multiple'] ? null File::class;
  91.             };
  92.         }
  93.         $emptyData = function (Options $options) {
  94.             return $options['multiple'] ? [] : null;
  95.         };
  96.         $resolver->setDefaults([
  97.             'compound' => false,
  98.             'data_class' => $dataClass,
  99.             'empty_data' => $emptyData,
  100.             'multiple' => false,
  101.             'allow_file_upload' => true,
  102.             'invalid_message' => 'Please select a valid file.',
  103.         ]);
  104.     }
  105.     public function getBlockPrefix(): string
  106.     {
  107.         return 'file';
  108.     }
  109.     private function getFileUploadError(int $errorCode)
  110.     {
  111.         $messageParameters = [];
  112.         if (\UPLOAD_ERR_INI_SIZE === $errorCode) {
  113.             [$limitAsString$suffix] = $this->factorizeSizes(0self::getMaxFilesize());
  114.             $messageTemplate 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.';
  115.             $messageParameters = [
  116.                 '{{ limit }}' => $limitAsString,
  117.                 '{{ suffix }}' => $suffix,
  118.             ];
  119.         } elseif (\UPLOAD_ERR_FORM_SIZE === $errorCode) {
  120.             $messageTemplate 'The file is too large.';
  121.         } else {
  122.             $messageTemplate 'The file could not be uploaded.';
  123.         }
  124.         if (null !== $this->translator) {
  125.             $message $this->translator->trans($messageTemplate$messageParameters'validators');
  126.         } else {
  127.             $message strtr($messageTemplate$messageParameters);
  128.         }
  129.         return new FileUploadError($message$messageTemplate$messageParameters);
  130.     }
  131.     /**
  132.      * Returns the maximum size of an uploaded file as configured in php.ini.
  133.      *
  134.      * This method should be kept in sync with Symfony\Component\HttpFoundation\File\UploadedFile::getMaxFilesize().
  135.      */
  136.     private static function getMaxFilesize(): int|float
  137.     {
  138.         $iniMax strtolower(\ini_get('upload_max_filesize'));
  139.         if ('' === $iniMax) {
  140.             return \PHP_INT_MAX;
  141.         }
  142.         $max ltrim($iniMax'+');
  143.         if (str_starts_with($max'0x')) {
  144.             $max \intval($max16);
  145.         } elseif (str_starts_with($max'0')) {
  146.             $max \intval($max8);
  147.         } else {
  148.             $max = (int) $max;
  149.         }
  150.         switch (substr($iniMax, -1)) {
  151.             case 't'$max *= 1024;
  152.                 // no break
  153.             case 'g'$max *= 1024;
  154.                 // no break
  155.             case 'm'$max *= 1024;
  156.                 // no break
  157.             case 'k'$max *= 1024;
  158.         }
  159.         return $max;
  160.     }
  161.     /**
  162.      * Converts the limit to the smallest possible number
  163.      * (i.e. try "MB", then "kB", then "bytes").
  164.      *
  165.      * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::factorizeSizes().
  166.      */
  167.     private function factorizeSizes(int $sizeint|float $limit)
  168.     {
  169.         $coef self::MIB_BYTES;
  170.         $coefFactor self::KIB_BYTES;
  171.         $limitAsString = (string) ($limit $coef);
  172.         // Restrict the limit to 2 decimals (without rounding! we
  173.         // need the precise value)
  174.         while (self::moreDecimalsThan($limitAsString2)) {
  175.             $coef /= $coefFactor;
  176.             $limitAsString = (string) ($limit $coef);
  177.         }
  178.         // Convert size to the same measure, but round to 2 decimals
  179.         $sizeAsString = (string) round($size $coef2);
  180.         // If the size and limit produce the same string output
  181.         // (due to rounding), reduce the coefficient
  182.         while ($sizeAsString === $limitAsString) {
  183.             $coef /= $coefFactor;
  184.             $limitAsString = (string) ($limit $coef);
  185.             $sizeAsString = (string) round($size $coef2);
  186.         }
  187.         return [$limitAsStringself::SUFFIXES[$coef]];
  188.     }
  189.     /**
  190.      * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::moreDecimalsThan().
  191.      */
  192.     private static function moreDecimalsThan(string $doubleint $numberOfDecimals): bool
  193.     {
  194.         return \strlen($double) > \strlen(round($double$numberOfDecimals));
  195.     }
  196. }