From 1a21ca73625fe19d056fde1e7ca1ebd5919bd597 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 6 Apr 2019 14:28:07 +0200 Subject: [PATCH 01/12] turn failed file uploads into form errors --- .../Form/Extension/Core/Type/FileType.php | 115 +++++++++++++++++ .../HttpFoundationRequestHandler.php | 13 ++ .../Component/Form/NativeRequestHandler.php | 24 ++++ .../Form/Tests/AbstractRequestHandlerTest.php | 24 ++++ .../Extension/Core/Type/FileTypeTest.php | 122 ++++++++++++++++++ .../HttpFoundationRequestHandlerTest.php | 5 + .../Form/Tests/NativeRequestHandlerTest.php | 11 ++ 7 files changed, 314 insertions(+) diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php index 59c72889d6..a7f912d5e3 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php @@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\Core\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; @@ -22,6 +23,15 @@ use Symfony\Component\OptionsResolver\OptionsResolver; class FileType extends AbstractType { + const KIB_BYTES = 1024; + const MIB_BYTES = 1048576; + + private static $suffixes = [ + 1 => 'bytes', + self::KIB_BYTES => 'KiB', + self::MIB_BYTES => 'MiB', + ]; + /** * {@inheritdoc} */ @@ -43,6 +53,10 @@ class FileType extends AbstractType foreach ($files as $file) { if ($requestHandler->isFileUpload($file)) { $data[] = $file; + + if (method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($file)) { + $form->addError($this->getFileUploadError($errorCode)); + } } } @@ -54,6 +68,8 @@ class FileType extends AbstractType } $event->setData($data); + } elseif ($requestHandler->isFileUpload($event->getData()) && method_exists($requestHandler, 'getUploadFileError') && null !== $errorCode = $requestHandler->getUploadFileError($event->getData())) { + $form->addError($this->getFileUploadError($errorCode)); } elseif (!$requestHandler->isFileUpload($event->getData())) { $event->setData(null); } @@ -116,4 +132,103 @@ class FileType extends AbstractType { return 'file'; } + + private function getFileUploadError($errorCode) + { + $messageParameters = []; + + if (UPLOAD_ERR_INI_SIZE === $errorCode) { + list($limitAsString, $suffix) = $this->factorizeSizes(0, self::getMaxFilesize()); + $messageTemplate = 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.'; + $messageParameters = [ + '{{ limit }}' => $limitAsString, + '{{ suffix }}' => $suffix, + ]; + } elseif (UPLOAD_ERR_FORM_SIZE === $errorCode) { + $messageTemplate = 'The file is too large.'; + } else { + $messageTemplate = 'The file could not be uploaded.'; + } + + return new FormError($messageTemplate, $messageTemplate, $messageParameters); + } + + /** + * Returns the maximum size of an uploaded file as configured in php.ini. + * + * This method should be kept in sync with Symfony\Component\HttpFoundation\File\UploadedFile::getMaxFilesize(). + * + * @return int The maximum size of an uploaded file in bytes + */ + private static function getMaxFilesize() + { + $iniMax = strtolower(ini_get('upload_max_filesize')); + + if ('' === $iniMax) { + return PHP_INT_MAX; + } + + $max = ltrim($iniMax, '+'); + if (0 === strpos($max, '0x')) { + $max = \intval($max, 16); + } elseif (0 === strpos($max, '0')) { + $max = \intval($max, 8); + } else { + $max = (int) $max; + } + + switch (substr($iniMax, -1)) { + case 't': $max *= 1024; + // no break + case 'g': $max *= 1024; + // no break + case 'm': $max *= 1024; + // no break + case 'k': $max *= 1024; + } + + return $max; + } + + /** + * Converts the limit to the smallest possible number + * (i.e. try "MB", then "kB", then "bytes"). + * + * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::factorizeSizes(). + */ + private function factorizeSizes($size, $limit) + { + $coef = self::MIB_BYTES; + $coefFactor = self::KIB_BYTES; + + $limitAsString = (string) ($limit / $coef); + + // Restrict the limit to 2 decimals (without rounding! we + // need the precise value) + while (self::moreDecimalsThan($limitAsString, 2)) { + $coef /= $coefFactor; + $limitAsString = (string) ($limit / $coef); + } + + // Convert size to the same measure, but round to 2 decimals + $sizeAsString = (string) round($size / $coef, 2); + + // If the size and limit produce the same string output + // (due to rounding), reduce the coefficient + while ($sizeAsString === $limitAsString) { + $coef /= $coefFactor; + $limitAsString = (string) ($limit / $coef); + $sizeAsString = (string) round($size / $coef, 2); + } + + return [$limitAsString, self::$suffixes[$coef]]; + } + + /** + * This method should be kept in sync with Symfony\Component\Validator\Constraints\FileValidator::moreDecimalsThan(). + */ + private static function moreDecimalsThan($double, $numberOfDecimals) + { + return \strlen((string) $double) > \strlen(round($double, $numberOfDecimals)); + } } diff --git a/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php b/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php index 75ee65443f..cf255792fe 100644 --- a/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php +++ b/src/Symfony/Component/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php @@ -17,6 +17,7 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\RequestHandlerInterface; use Symfony\Component\Form\Util\ServerParams; use Symfony\Component\HttpFoundation\File\File; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; /** @@ -115,4 +116,16 @@ class HttpFoundationRequestHandler implements RequestHandlerInterface { return $data instanceof File; } + + /** + * @return int|null + */ + public function getUploadFileError($data) + { + if (!$data instanceof UploadedFile || $data->isValid()) { + return null; + } + + return $data->getError(); + } } diff --git a/src/Symfony/Component/Form/NativeRequestHandler.php b/src/Symfony/Component/Form/NativeRequestHandler.php index f3c0d839fe..5997fba67d 100644 --- a/src/Symfony/Component/Form/NativeRequestHandler.php +++ b/src/Symfony/Component/Form/NativeRequestHandler.php @@ -135,6 +135,30 @@ class NativeRequestHandler implements RequestHandlerInterface return \is_array($data) && isset($data['error']) && \is_int($data['error']); } + /** + * @return int|null + */ + public function getUploadFileError($data) + { + if (!\is_array($data)) { + return null; + } + + if (!isset($data['error'])) { + return null; + } + + if (!\is_int($data['error'])) { + return null; + } + + if (UPLOAD_ERR_OK === $data['error']) { + return null; + } + + return $data['error']; + } + /** * Returns the method used to submit the request to the server. * diff --git a/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php b/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php index 16d4045e6d..b470769344 100644 --- a/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php +++ b/src/Symfony/Component/Form/Tests/AbstractRequestHandlerTest.php @@ -360,6 +360,28 @@ abstract class AbstractRequestHandlerTest extends TestCase $this->assertFalse($this->requestHandler->isFileUpload($this->getInvalidFile())); } + /** + * @dataProvider uploadFileErrorCodes + */ + public function testFailedFileUploadIsTurnedIntoFormError($errorCode, $expectedErrorCode) + { + $this->assertSame($expectedErrorCode, $this->requestHandler->getUploadFileError($this->getFailedUploadedFile($errorCode))); + } + + public function uploadFileErrorCodes() + { + return [ + 'no error' => [UPLOAD_ERR_OK, null], + 'upload_max_filesize ini directive' => [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_INI_SIZE], + 'MAX_FILE_SIZE from form' => [UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_FORM_SIZE], + 'partially uploaded' => [UPLOAD_ERR_PARTIAL, UPLOAD_ERR_PARTIAL], + 'no file upload' => [UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_FILE], + 'missing temporary directory' => [UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_NO_TMP_DIR], + 'write failure' => [UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_CANT_WRITE], + 'stopped by extension' => [UPLOAD_ERR_EXTENSION, UPLOAD_ERR_EXTENSION], + ]; + } + abstract protected function setRequestData($method, $data, $files = []); abstract protected function getRequestHandler(); @@ -368,6 +390,8 @@ abstract class AbstractRequestHandlerTest extends TestCase abstract protected function getInvalidFile(); + abstract protected function getFailedUploadedFile($errorCode); + protected function createForm($name, $method = null, $compound = false) { $config = $this->createBuilder($name, $compound); diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php index ea012c451e..73d39ace02 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/FileTypeTest.php @@ -184,6 +184,128 @@ class FileTypeTest extends BaseTypeTest ]; } + /** + * @dataProvider uploadFileErrorCodes + */ + public function testFailedFileUploadIsTurnedIntoFormErrorUsingHttpFoundationRequestHandler($errorCode, $expectedErrorMessage) + { + $form = $this->factory + ->createBuilder(static::TESTED_TYPE) + ->setRequestHandler(new HttpFoundationRequestHandler()) + ->getForm(); + $form->submit(new UploadedFile(__DIR__.'/../../../Fixtures/foo', 'foo', null, null, $errorCode, true)); + + if (UPLOAD_ERR_OK === $errorCode) { + $this->assertTrue($form->isValid()); + } else { + $this->assertFalse($form->isValid()); + $this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage()); + } + } + + /** + * @dataProvider uploadFileErrorCodes + */ + public function testFailedFileUploadIsTurnedIntoFormErrorUsingNativeRequestHandler($errorCode, $expectedErrorMessage) + { + $form = $this->factory + ->createBuilder(static::TESTED_TYPE) + ->setRequestHandler(new NativeRequestHandler()) + ->getForm(); + $form->submit([ + 'name' => 'foo.txt', + 'type' => 'text/plain', + 'tmp_name' => 'owfdskjasdfsa', + 'error' => $errorCode, + 'size' => 100, + ]); + + if (UPLOAD_ERR_OK === $errorCode) { + $this->assertTrue($form->isValid()); + } else { + $this->assertFalse($form->isValid()); + $this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage()); + } + } + + /** + * @dataProvider uploadFileErrorCodes + */ + public function testMultipleSubmittedFailedFileUploadsAreTurnedIntoFormErrorUsingHttpFoundationRequestHandler($errorCode, $expectedErrorMessage) + { + $form = $this->factory + ->createBuilder(static::TESTED_TYPE, null, [ + 'multiple' => true, + ]) + ->setRequestHandler(new HttpFoundationRequestHandler()) + ->getForm(); + $form->submit([ + new UploadedFile(__DIR__.'/../../../Fixtures/foo', 'foo', null, null, $errorCode, true), + new UploadedFile(__DIR__.'/../../../Fixtures/foo', 'bar', null, null, $errorCode, true), + ]); + + if (UPLOAD_ERR_OK === $errorCode) { + $this->assertTrue($form->isValid()); + } else { + $this->assertFalse($form->isValid()); + $this->assertCount(2, $form->getErrors()); + $this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage()); + $this->assertSame($expectedErrorMessage, $form->getErrors()[1]->getMessage()); + } + } + + /** + * @dataProvider uploadFileErrorCodes + */ + public function testMultipleSubmittedFailedFileUploadsAreTurnedIntoFormErrorUsingNativeRequestHandler($errorCode, $expectedErrorMessage) + { + $form = $this->factory + ->createBuilder(static::TESTED_TYPE, null, [ + 'multiple' => true, + ]) + ->setRequestHandler(new NativeRequestHandler()) + ->getForm(); + $form->submit([ + [ + 'name' => 'foo.txt', + 'type' => 'text/plain', + 'tmp_name' => 'owfdskjasdfsa', + 'error' => $errorCode, + 'size' => 100, + ], + [ + 'name' => 'bar.txt', + 'type' => 'text/plain', + 'tmp_name' => 'owfdskjasdfsa', + 'error' => $errorCode, + 'size' => 100, + ], + ]); + + if (UPLOAD_ERR_OK === $errorCode) { + $this->assertTrue($form->isValid()); + } else { + $this->assertFalse($form->isValid()); + $this->assertCount(2, $form->getErrors()); + $this->assertSame($expectedErrorMessage, $form->getErrors()[0]->getMessage()); + $this->assertSame($expectedErrorMessage, $form->getErrors()[1]->getMessage()); + } + } + + public function uploadFileErrorCodes() + { + return [ + 'no error' => [UPLOAD_ERR_OK, null], + 'upload_max_filesize ini directive' => [UPLOAD_ERR_INI_SIZE, 'The file is too large. Allowed maximum size is {{ limit }} {{ suffix }}.'], + 'MAX_FILE_SIZE from form' => [UPLOAD_ERR_FORM_SIZE, 'The file is too large.'], + 'partially uploaded' => [UPLOAD_ERR_PARTIAL, 'The file could not be uploaded.'], + 'no file upload' => [UPLOAD_ERR_NO_FILE, 'The file could not be uploaded.'], + 'missing temporary directory' => [UPLOAD_ERR_NO_TMP_DIR, 'The file could not be uploaded.'], + 'write failure' => [UPLOAD_ERR_CANT_WRITE, 'The file could not be uploaded.'], + 'stopped by extension' => [UPLOAD_ERR_EXTENSION, 'The file could not be uploaded.'], + ]; + } + private function createUploadedFile(RequestHandlerInterface $requestHandler, $path, $originalName) { if ($requestHandler instanceof HttpFoundationRequestHandler) { diff --git a/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php index 2b13451183..0e5389568e 100644 --- a/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/HttpFoundation/HttpFoundationRequestHandlerTest.php @@ -56,4 +56,9 @@ class HttpFoundationRequestHandlerTest extends AbstractRequestHandlerTest { return 'file:///etc/passwd'; } + + protected function getFailedUploadedFile($errorCode) + { + return new UploadedFile(__DIR__.'/../../Fixtures/foo', 'foo', null, null, $errorCode, true); + } } diff --git a/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php b/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php index 25a4716650..36638a124f 100644 --- a/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php +++ b/src/Symfony/Component/Form/Tests/NativeRequestHandlerTest.php @@ -275,4 +275,15 @@ class NativeRequestHandlerTest extends AbstractRequestHandlerTest 'size' => '100', ]; } + + protected function getFailedUploadedFile($errorCode) + { + return [ + 'name' => 'upload.txt', + 'type' => 'text/plain', + 'tmp_name' => 'owfdskjasdfsa', + 'error' => $errorCode, + 'size' => 100, + ]; + } } From a5f1afca15e26b1b886d85a357e1370d7c479e47 Mon Sep 17 00:00:00 2001 From: Titouan Galopin Date: Sat, 6 Apr 2019 19:05:56 +0200 Subject: [PATCH 02/12] [Translator] Warm up the translations cache in dev --- .../Component/Translation/DataCollectorTranslator.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Translation/DataCollectorTranslator.php b/src/Symfony/Component/Translation/DataCollectorTranslator.php index 6f826dfaa6..c0cc05996a 100644 --- a/src/Symfony/Component/Translation/DataCollectorTranslator.php +++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php @@ -11,12 +11,13 @@ namespace Symfony\Component\Translation; +use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; /** * @author Abdellatif Ait boudad */ -class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface +class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, WarmableInterface { const MESSAGE_DEFINED = 0; const MESSAGE_MISSING = 1; @@ -87,6 +88,14 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter return $this->translator->getCatalogue($locale); } + /** + * {@inheritdoc} + */ + public function warmUp($cacheDir) + { + return $this->translator->warmUp($cacheDir); + } + /** * Gets the fallback locales. * From 7113a53e197193537eabea85739a61d7304c5148 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 6 Apr 2019 19:28:56 +0200 Subject: [PATCH 03/12] fix horizontal spacing of inlined Bootstrap forms --- .../Resources/views/Form/bootstrap_3_layout.html.twig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig index e1164cdfbc..708e149bce 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig @@ -108,10 +108,10 @@ {% block form_row -%}
- {{- form_label(form) -}} - {{- form_widget(form) -}} - {{- form_errors(form) -}} -
+ {{- form_label(form) }} {# -#} + {{ form_widget(form) }} {# -#} + {{ form_errors(form) }} {# -#} + {# -#} {%- endblock form_row %} {% block button_row -%} From a53e0fe1cdce66ad9ebb9a25e661337e006dfb52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sat, 6 Apr 2019 21:12:52 +0200 Subject: [PATCH 04/12] Use a class name that does not actually exist Using "Foo", a class name that corresponds to no less than 22 fixture classes, results in the first found "Foo" being loaded when one is found by the ClassNotFoundFatalErrorHandler error handler, I am not sure exactly why, but it is not really a big issue because this is a fatal error handler and execution is not supposed to continue after that. Except that is very much the case when running the whole test suite sequentially with ./phpunit . Then we arrive to the DI component test suite, and a failure happens because \\foo was not supposed to be defined: > Failed asserting that exception message 'The definition for "\foo" has > no class attribute, and appears to reference a class or interface in the > global namespace. Leaving out the "class" attribute is only allowed for > namespaced classes. Please specify the class attribute explicitly to get > rid of this error.' contains 'The definition for "\foo" has no class.'. --- src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index c1dea75bbd..15de37c7b7 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -525,7 +525,7 @@ class ErrorHandlerTest extends TestCase */ public function testHandleErrorException() { - $exception = new \Error("Class 'Foo' not found"); + $exception = new \Error("Class 'IReallyReallyDoNotExistAnywhereInTheRepositoryISwear' not found"); $handler = new ErrorHandler(); $handler->setExceptionHandler(function () use (&$args) { @@ -535,7 +535,7 @@ class ErrorHandlerTest extends TestCase $handler->handleException($exception); $this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $args[0]); - $this->assertStringStartsWith("Attempted to load class \"Foo\" from the global namespace.\nDid you forget a \"use\" statement", $args[0]->getMessage()); + $this->assertStringStartsWith("Attempted to load class \"IReallyReallyDoNotExistAnywhereInTheRepositoryISwear\" from the global namespace.\nDid you forget a \"use\" statement", $args[0]->getMessage()); } /** From 6d51a04b11ee6cf85cdc5c1b6fb36ab1cb456308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sat, 6 Apr 2019 18:22:13 +0200 Subject: [PATCH 05/12] Run test in separate process This test calls code that defines some environment variables, which in turn trigger the registration of a the deprecation error handler, which causes unexpected issues when testing other components. --- src/Symfony/Component/BrowserKit/Tests/ClientTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Symfony/Component/BrowserKit/Tests/ClientTest.php b/src/Symfony/Component/BrowserKit/Tests/ClientTest.php index ab6d118589..a21a9481a7 100644 --- a/src/Symfony/Component/BrowserKit/Tests/ClientTest.php +++ b/src/Symfony/Component/BrowserKit/Tests/ClientTest.php @@ -621,6 +621,9 @@ class ClientTest extends TestCase $this->assertEquals([], $client->getCookieJar()->all(), '->restart() clears the cookies'); } + /** + * @runInSeparateProcess + */ public function testInsulatedRequests() { $client = new TestClient(); From e55f6e6f5e63cf840caaf592e8b5d7934ffb953c Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 6 Apr 2019 20:39:34 +0200 Subject: [PATCH 06/12] fix tests --- .../Tests/Compiler/AutowirePassTest.php | 10 ++++-- .../Compiler/ResolveBindingsPassTest.php | 33 +++---------------- .../Translation/DataCollectorTranslator.php | 4 ++- .../Component/Translation/composer.json | 2 ++ 4 files changed, 17 insertions(+), 32 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php index 6235ed50be..31fa665ae7 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php @@ -21,9 +21,9 @@ use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic; use Symfony\Component\DependencyInjection\TypedReference; -use Symfony\Component\HttpKernel\HttpKernelInterface; require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php'; @@ -606,13 +606,17 @@ class AutowirePassTest extends TestCase ); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @exceptedExceptionMessage Invalid service "Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy": method "setLogger()" does not exist. + */ public function testWithNonExistingSetterAndAutowiring() { $container = new ContainerBuilder(); - $definition = $container->register(HttpKernelInterface::class, HttpKernelInterface::class)->setAutowired(true); + $definition = $container->register(CaseSensitiveClass::class, CaseSensitiveClass::class)->setAutowired(true); $definition->addMethodCall('setLogger'); - $this->expectException(RuntimeException::class); + (new ResolveClassPass())->process($container); (new AutowireRequiredMethodsPass())->process($container); (new AutowirePass())->process($container); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php index 84b3d6c652..7bbecf6207 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php @@ -17,13 +17,11 @@ use Symfony\Component\DependencyInjection\Compiler\AutowireRequiredMethodsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; use Symfony\Component\DependencyInjection\Tests\Fixtures\ParentNotExists; use Symfony\Component\DependencyInjection\TypedReference; -use Symfony\Component\HttpKernel\HttpKernelInterface; require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php'; @@ -115,6 +113,10 @@ class ResolveBindingsPassTest extends TestCase $this->assertEquals([['setDefaultLocale', ['fr']]], $definition->getMethodCalls()); } + /** + * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException + * @exceptedExceptionMessage Invalid service "Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy": method "setLogger()" does not exist. + */ public function testWithNonExistingSetterAndBinding() { $container = new ContainerBuilder(); @@ -123,36 +125,11 @@ class ResolveBindingsPassTest extends TestCase '$c' => (new Definition('logger'))->setFactory('logger'), ]; - $definition = $container->register(HttpKernelInterface::class, HttpKernelInterface::class); + $definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class); $definition->addMethodCall('setLogger'); $definition->setBindings($bindings); - $this->expectException(RuntimeException::class); $pass = new ResolveBindingsPass(); $pass->process($container); } - - public function testTupleBinding() - { - $container = new ContainerBuilder(); - - $bindings = [ - '$c' => new BoundArgument(new Reference('bar')), - CaseSensitiveClass::class.'$c' => new BoundArgument(new Reference('foo')), - ]; - - $definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class); - $definition->addMethodCall('setSensitiveClass'); - $definition->addMethodCall('setAnotherC'); - $definition->setBindings($bindings); - - $pass = new ResolveBindingsPass(); - $pass->process($container); - - $expected = [ - ['setSensitiveClass', [new Reference('foo')]], - ['setAnotherC', [new Reference('bar')]], - ]; - $this->assertEquals($expected, $definition->getMethodCalls()); - } } diff --git a/src/Symfony/Component/Translation/DataCollectorTranslator.php b/src/Symfony/Component/Translation/DataCollectorTranslator.php index c0cc05996a..7eaf928e7f 100644 --- a/src/Symfony/Component/Translation/DataCollectorTranslator.php +++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php @@ -93,7 +93,9 @@ class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInter */ public function warmUp($cacheDir) { - return $this->translator->warmUp($cacheDir); + if ($this->translator instanceof WarmableInterface) { + $this->translator->warmUp($cacheDir); + } } /** diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 45ff664ca4..93c1236f27 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -22,7 +22,9 @@ "require-dev": { "symfony/config": "~2.8|~3.0|~4.0", "symfony/dependency-injection": "~3.4|~4.0", + "symfony/http-kernel": "~3.4|~4.0", "symfony/intl": "^2.8.18|^3.2.5|~4.0", + "symfony/var-dumper": "~3.4|~4.0", "symfony/yaml": "~3.4|~4.0", "symfony/finder": "~2.8|~3.0|~4.0", "psr/log": "~1.0" From 6a732dc0316b51812b5353eb5a2bc211d383c375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Egyed?= Date: Sat, 6 Apr 2019 23:15:33 +0200 Subject: [PATCH 07/12] [Validator] Translate unique collection message to Hungarian --- .../Validator/Resources/translations/validators.hu.xlf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf index c527c58d2d..50d433f665 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf @@ -334,6 +334,10 @@ This value should be valid JSON. Ez az érték érvényes JSON kell, hogy legyen. + + This collection should contain only unique elements. + Ez a gyűjtemény csak egyedi elemeket tartalmazhat. + From 79b1fb83338b73af25f1949bfcea54a6b74905c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sun, 7 Apr 2019 09:54:46 +0200 Subject: [PATCH 08/12] Handle case where no translations were found Right now, the program emits a warning when trying to use max() on an empty array. --- .../Translation/Resources/bin/translation-status.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Symfony/Component/Translation/Resources/bin/translation-status.php b/src/Symfony/Component/Translation/Resources/bin/translation-status.php index acc8b4e227..0d37c3e0aa 100644 --- a/src/Symfony/Component/Translation/Resources/bin/translation-status.php +++ b/src/Symfony/Component/Translation/Resources/bin/translation-status.php @@ -159,6 +159,11 @@ function printTitle($title) function printTable($translations, $verboseOutput) { + if (0 === count($translations)) { + echo 'No translations found'; + + return; + } $longestLocaleNameLength = max(array_map('strlen', array_keys($translations))); foreach ($translations as $locale => $translation) { From 7db9200279319a5730d7bd44cd2db137d35dde43 Mon Sep 17 00:00:00 2001 From: Philipp Cordes Date: Sun, 6 Jan 2019 15:04:00 +0100 Subject: [PATCH 09/12] [Validator] Only traverse arrays that are cascaded into --- .../Validator/Constraints/Collection.php | 2 +- .../Tests/Validator/AbstractValidatorTest.php | 24 +++++++++++++++++++ .../RecursiveContextualValidator.php | 17 +++++-------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/Validator/Constraints/Collection.php b/src/Symfony/Component/Validator/Constraints/Collection.php index 21427722d9..86418690b8 100644 --- a/src/Symfony/Component/Validator/Constraints/Collection.php +++ b/src/Symfony/Component/Validator/Constraints/Collection.php @@ -68,7 +68,7 @@ class Collection extends Composite } if (!$field instanceof Optional && !$field instanceof Required) { - $this->fields[$fieldName] = $field = new Required($field); + $this->fields[$fieldName] = new Required($field); } } } diff --git a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php index ecfc10feb6..884ccc7da0 100644 --- a/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php +++ b/src/Symfony/Component/Validator/Tests/Validator/AbstractValidatorTest.php @@ -589,6 +589,30 @@ abstract class AbstractValidatorTest extends TestCase $this->assertNull($violations[0]->getCode()); } + public function testOnlyCascadedArraysAreTraversed() + { + $entity = new Entity(); + $entity->reference = ['key' => new Reference()]; + + $callback = function ($value, ExecutionContextInterface $context) { + $context->addViolation('Message %param%', ['%param%' => 'value']); + }; + + $this->metadata->addPropertyConstraint('reference', new Callback([ + 'callback' => function () {}, + 'groups' => 'Group', + ])); + $this->referenceMetadata->addConstraint(new Callback([ + 'callback' => $callback, + 'groups' => 'Group', + ])); + + $violations = $this->validate($entity, null, 'Group'); + + /* @var ConstraintViolationInterface[] $violations */ + $this->assertCount(0, $violations); + } + public function testArrayTraversalCannotBeDisabled() { $entity = new Entity(); diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index 50253c50e1..dc139c36d4 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -352,24 +352,18 @@ class RecursiveContextualValidator implements ContextualValidatorInterface * Validates each object in a collection against the constraints defined * for their classes. * - * If the parameter $recursive is set to true, nested {@link \Traversable} - * objects are iterated as well. Nested arrays are always iterated, - * regardless of the value of $recursive. + * Nested arrays are also iterated. * * @param iterable $collection The collection * @param string $propertyPath The current property path * @param (string|GroupSequence)[] $groups The validated groups * @param ExecutionContextInterface $context The current execution context - * - * @see ClassNode - * @see CollectionNode */ private function validateEachObjectIn($collection, $propertyPath, array $groups, ExecutionContextInterface $context) { foreach ($collection as $key => $value) { if (\is_array($value)) { - // Arrays are always cascaded, independent of the specified - // traversal strategy + // Also traverse nested arrays $this->validateEachObjectIn( $value, $propertyPath.'['.$key.']', @@ -599,7 +593,8 @@ class RecursiveContextualValidator implements ContextualValidatorInterface * in the passed metadata object. Then, if the value is an instance of * {@link \Traversable} and the selected traversal strategy permits it, * the value is traversed and each nested object validated against its own - * constraints. Arrays are always traversed. + * constraints. If the value is an array, it is traversed regardless of + * the given strategy. * * @param mixed $value The validated value * @param object|null $object The current object @@ -658,8 +653,8 @@ class RecursiveContextualValidator implements ContextualValidatorInterface $cascadingStrategy = $metadata->getCascadingStrategy(); - // Quit unless we have an array or a cascaded object - if (!\is_array($value) && !($cascadingStrategy & CascadingStrategy::CASCADE)) { + // Quit unless we cascade + if (!($cascadingStrategy & CascadingStrategy::CASCADE)) { return; } From 331b601fed1cec3d323b7f365878bb830c2d878c Mon Sep 17 00:00:00 2001 From: Hugo Hamon Date: Sun, 7 Apr 2019 09:35:09 +0200 Subject: [PATCH 10/12] [3.4] [Validator] Add missing french validation translations. --- .../Resources/translations/validators.fr.xlf | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf index 7b1799d533..d1601f72df 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf @@ -334,6 +334,34 @@ This value should be valid JSON. Cette valeur doit être un JSON valide. + + This collection should contain only unique elements. + Cette collection ne doit pas comporter de doublons. + + + This value should be positive. + Cette valeur doit être strictement positive. + + + This value should be either positive or zero. + Cette valeur doit être supérieure ou égale à zéro. + + + This value should be negative. + Cette valeur doit être strictement négative. + + + This value should be either negative or zero. + Cette valeur doit être inférieure ou égale à zéro. + + + This value is not a valid timezone. + Cette valeur n'est pas un fuseau horaire valide. + + + This password has been leaked in a data breach, it must not be used. Please use another password. + Ce mot de passe a été révélé par une faille de sécurité et ne doit plus être utilisé. Veuillez utiliser un autre mot de passe. + From 62e5a91150968f03117f3b85045d9b11f1a057ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Egyed?= Date: Sat, 6 Apr 2019 23:31:05 +0200 Subject: [PATCH 11/12] [Validator] Add missing Hungarian translations --- .../Resources/translations/validators.hu.xlf | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf index 50d433f665..300eb5f68f 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf @@ -335,9 +335,29 @@ Ez az érték érvényes JSON kell, hogy legyen. + This value should be positive. + Ennek az értéknek pozitívnak kell lennie. + + + This value should be either positive or zero. + Ennek az értéknek pozitívnak vagy nullának kell lennie. + + + This value should be negative. + Ennek az értéknek negatívnak kell lennie. + + + This value should be either negative or zero. + Ennek az értéknek negatívnak vagy nullának kell lennie. + + This collection should contain only unique elements. Ez a gyűjtemény csak egyedi elemeket tartalmazhat. + + This value is not a valid timezone. + Ez az érték nem egy érvényes időzóna. + From 9c41842756d4313025c0d2cecbf74daf3c1494a9 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sun, 7 Apr 2019 11:15:59 +0200 Subject: [PATCH 12/12] fix translating file validation error message --- .../FrameworkBundle/Resources/config/form.xml | 2 +- .../Form/Extension/Core/Type/FileType.php | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index 7dd51bbed6..5088554ff1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -93,7 +93,7 @@ The "%service_id%" service is deprecated since Symfony 3.1 and will be removed in 4.0. - The "%service_id%" service is deprecated since Symfony 3.1 and will be removed in 4.0. + The "%service_id%" service is deprecated since Symfony 3.1 and will be removed in 4.0. diff --git a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php index a7f912d5e3..f8afce2ee5 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/FileType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/FileType.php @@ -20,6 +20,7 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Translation\TranslatorInterface; class FileType extends AbstractType { @@ -32,6 +33,13 @@ class FileType extends AbstractType self::MIB_BYTES => 'MiB', ]; + private $translator; + + public function __construct(TranslatorInterface $translator = null) + { + $this->translator = $translator; + } + /** * {@inheritdoc} */ @@ -150,7 +158,13 @@ class FileType extends AbstractType $messageTemplate = 'The file could not be uploaded.'; } - return new FormError($messageTemplate, $messageTemplate, $messageParameters); + if (null !== $this->translator) { + $message = $this->translator->trans($messageTemplate, $messageParameters); + } else { + $message = strtr($messageTemplate, $messageParameters); + } + + return new FormError($message, $messageTemplate, $messageParameters); } /**