diff --git a/.github/rm-invalid-lowest-lock-files.php b/.github/rm-invalid-lowest-lock-files.php deleted file mode 100644 index 80cf81b9340ce..0000000000000 --- a/.github/rm-invalid-lowest-lock-files.php +++ /dev/null @@ -1,158 +0,0 @@ - [], 'packages-dev' => []]; - $composerJsons[$composerJson['name']] = [$dir, $composerLock['packages'] + $composerLock['packages-dev'], getRelevantContent($composerJson)]; -} - -$referencedCommits = []; - -foreach ($composerJsons as list($dir, $lockedPackages)) { - foreach ($lockedPackages as $lockedJson) { - if (0 !== strpos($version = $lockedJson['version'], 'dev-') && '-dev' !== substr($version, -4)) { - continue; - } - - if (!isset($composerJsons[$name = $lockedJson['name']])) { - echo "$dir/composer.lock references missing $name.\n"; - @unlink($dir.'/composer.lock'); - continue 2; - } - - if (isset($composerJsons[$name][2]['repositories']) && !isset($lockedJson['repositories'])) { - // the locked package has been patched locally but the lock references a commit, - // which means the referencing package itself is not modified - continue; - } - - foreach (['minimum-stability', 'prefer-stable'] as $key) { - if (array_key_exists($key, $composerJsons[$name][2])) { - $lockedJson[$key] = $composerJsons[$name][2][$key]; - } - } - - // use weak comparison to ignore ordering - if (getRelevantContent($lockedJson) != $composerJsons[$name][2]) { - echo "$dir/composer.lock is not in sync with $name.\n"; - @unlink($dir.'/composer.lock'); - continue 2; - } - - if ($lockedJson['dist']['reference']) { - $referencedCommits[$name][$lockedJson['dist']['reference']][] = $dir; - } - } -} - -if (!$referencedCommits) { - return; -} - -@mkdir($_SERVER['HOME'].'/.cache/composer/repo/https---repo.packagist.org', 0777, true); - -$ch = null; -$mh = curl_multi_init(); -$sh = curl_share_init(); -curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); -curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); -curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); -$chs = []; - -foreach ($referencedCommits as $name => $dirsByCommit) { - $chs[] = $ch = [curl_init(), fopen($_SERVER['HOME'].'/.cache/composer/repo/https---repo.packagist.org/provider-'.strtr($name, '/', '$').'.json', 'wb')]; - curl_setopt($ch[0], CURLOPT_URL, 'https://repo.packagist.org/p/'.$name.'.json'); - curl_setopt($ch[0], CURLOPT_FILE, $ch[1]); - curl_setopt($ch[0], CURLOPT_SHARE, $sh); - curl_multi_add_handle($mh, $ch[0]); -} - -do { - curl_multi_exec($mh, $active); - curl_multi_select($mh); -} while ($active); - -foreach ($chs as list($ch, $fd)) { - curl_multi_remove_handle($mh, $ch); - curl_close($ch); - fclose($fd); -} - -foreach ($referencedCommits as $name => $dirsByCommit) { - $repo = file_get_contents($_SERVER['HOME'].'/.cache/composer/repo/https---repo.packagist.org/provider-'.strtr($name, '/', '$').'.json'); - $repo = json_decode($repo, true); - - foreach ($repo['packages'][$name] as $version) { - unset($referencedCommits[$name][$version['source']['reference']]); - } -} - -foreach ($referencedCommits as $name => $dirsByCommit) { - foreach ($dirsByCommit as $dirs) { - foreach ($dirs as $dir) { - if (file_exists($dir.'/composer.lock')) { - echo "$dir/composer.lock references old commit for $name.\n"; - @unlink($dir.'/composer.lock'); - } - } - } -} diff --git a/.travis.yml b/.travis.yml index 009143743f598..bcb5e9ff8f69b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -279,11 +279,7 @@ install: [[ ! $X ]] || (exit 1) elif [[ $deps = low ]]; then - [[ -e ~/php-ext/composer-lowest.lock.tar ]] && tar -xf ~/php-ext/composer-lowest.lock.tar - tar -cf ~/php-ext/composer-lowest.lock.tar --files-from /dev/null - php .github/rm-invalid-lowest-lock-files.php $COMPONENTS - echo "$COMPONENTS" | parallel --gnu "tfold {} 'cd {} && ([ -e composer.lock ] && ${COMPOSER_UP/update/install} || $COMPOSER_UP --prefer-lowest --prefer-stable) && $PHPUNIT_X'" - echo "$COMPONENTS" | xargs -n1 -I{} tar --append -f ~/php-ext/composer-lowest.lock.tar {}/composer.lock + echo "$COMPONENTS" | parallel --gnu "tfold {} 'cd {} && $COMPOSER_UP --prefer-lowest --prefer-stable && $PHPUNIT_X'" else if [[ $PHP = 8.0* ]]; then # add return types before running the test suite diff --git a/CHANGELOG-5.2.md b/CHANGELOG-5.2.md index 68a57b4be09f1..5e5f455254f7a 100644 --- a/CHANGELOG-5.2.md +++ b/CHANGELOG-5.2.md @@ -7,6 +7,46 @@ in 5.2 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v5.2.0...v5.2.1 +* 5.2.6 (2021-03-29) + + * bug #40598 [Form] error if the input string couldn't be parsed as a date (xabbuh) + * bug #40587 [HttpClient] fix using stream_copy_to_stream() with responses cast to php streams (nicolas-grekas) + * bug #40510 [Form] IntegerType: Always use en for IntegerToLocalizedStringTransformer (Warxcell) + * bug #40593 Uses the correct assignment action for console options depending if they are short or long (topikito) + * bug #40535 [HttpKernel] ConfigDataCollector to return known data without the need of a Kernel (topikito) + * bug #40552 [Translation] Fix update existing key with existing +int-icu domain (Alexis) + * bug #40541 Fixed parsing deprecated definitions without message key (adamwojs) + * bug #40537 [Security] Handle properly 'auto' option for remember me cookie security (fliespl) + * bug #40524 [Console] fix emojis messing up the line width (MarionLeHerisson) + * bug #40506 [Validator] Avoid triggering the autoloader for user-input values (Seldaek) + * bug #40544 [FrameworkBundle] ensure TestBrowserToken::$firewallName is serialized (kbond) + * bug #40547 [RateLimiter] Security hardening - Rate limiter (jderusse) + * bug #40538 [HttpClient] remove using $http_response_header (nicolas-grekas) + * bug #40508 [PhpUnitBridge] fix reporting deprecations from DebugClassLoader (nicolas-grekas) + * bug #40497 [HttpFoundation] enable HTTP method overrides as early as possible with the HTTP cache (xabbuh) + * bug #40348 [Console] Fix line wrapping for decorated text in block output (grasmash) + * bug #40499 [Inflector][String] Fixed pluralize "coupon" (Nyholm) + * bug #40494 [PhpUnitBridge] fix compat with symfony/debug (nicolas-grekas) + * bug #40453 [VarDumper] Adds support for ReflectionUnionType to VarDumper (Michael Nelson, michaeldnelson) + * bug #40460 Correctly clear lines for multi-line progress bar messages (grasmash) + * bug #40490 [Security] Add XML support for authenticator manager (wouterj) + * bug #40242 [ErrorHandler] Fix error caused by `include` + open_basedir (stlrnz) + * bug #40368 [FrameworkBundle] Make the TestBrowserToken interchangeable with other tokens (Seldaek) + * bug #40450 [Console] ProgressBar clears too many lines on update (danepowell) + * bug #40178 [FrameworkBundle] Exclude unreadable files when executing About command (michaljusiega) + * bug #40472 [Bridge\Twig] Add 'form-control-range' for range input type (Oviglo) + * bug #40481 make async-ses required (jderusse) + * bug #39866 [Mime] Escape commas in address names (YaFou) + * bug #40373 Check if templating engine supports given view (fritzmg) + * bug #39992 [Security] Refresh original user in SwitchUserListener (AndrolGenhald) + * bug #40446 [TwigBridge] Fix "Serialization of 'Closure'" error when rendering an TemplatedEmail (jderusse) + * bug #40416 Fix `ConstraintViolation#getMessageTemplate()` to always return `string` (Ocramius) + * bug #40425 [DoctrineBridge] Fix eventListener initialization when eventSubscriber constructor dispatch an event (jderusse) + * bug #40313 [FrameworkBundle] Fix PropertyAccess definition when not in debug (PedroTroller) + * bug #40417 [Form] clear unchecked choice radio boxes even if clear missing is set to false (xabbuh) + * bug #40388 [ErrorHandler] Added missing type annotations to FlattenException (derrabus) + * bug #40407 [TwigBridge] Allow version 3 of the Twig extra packages (derrabus) + * 5.2.5 (2021-03-10) * bug #40415 Fix `ConstraintViolation#getPropertyPath()` to always return `string` (Ocramius) diff --git a/composer.json b/composer.json index fc7535421b83b..fcc78bf789953 100644 --- a/composer.json +++ b/composer.json @@ -149,9 +149,9 @@ "symfony/phpunit-bridge": "^5.2", "symfony/security-acl": "~2.8|~3.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "twig/cssinliner-extra": "^2.12", - "twig/inky-extra": "^2.12", - "twig/markdown-extra": "^2.12" + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" }, "conflict": { "doctrine/dbal": "<2.10", diff --git a/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php b/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php index 9b3c1595a41f0..1ee4f54ded8e1 100644 --- a/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php +++ b/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php @@ -177,6 +177,7 @@ private function initializeSubscribers() if (!isset($this->listeners[$event])) { $this->listeners[$event] = []; } + unset($this->initialized[$event]); $this->listeners[$event] += $listeners; } $this->subscribers = []; diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php index 3e3ebef48c7c3..b4d79e7ba46d7 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php @@ -11,6 +11,8 @@ namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\TestSuite; use PHPUnit\Util\Test; use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerFor; use Symfony\Component\Debug\DebugClassLoader as LegacyDebugClassLoader; @@ -81,47 +83,56 @@ public function __construct($message, array $trace, $file) $this->triggeringFile = isset($trace[1 + $j]['args'][1]) ? realpath($trace[1 + $j]['args'][1]) : (new \ReflectionClass($class))->getFileName(); $this->getOriginalFilesStack(); array_splice($this->originalFilesStack, 0, $j, [$this->triggeringFile]); + + if (preg_match('/(?|"([^"]++)" that is deprecated|should implement method "(?:static )?([^:]++))/', $message, $m) || preg_match('/^(?:The|Method) "([^":]++)/', $message, $m)) { + $this->triggeringFile = (new \ReflectionClass($m[1]))->getFileName(); + array_unshift($this->originalFilesStack, $this->triggeringFile); + } } break; } } - if (isset($line['object']) || isset($line['class'])) { - set_error_handler(function () {}); - $parsedMsg = unserialize($this->message); - restore_error_handler(); - if ($parsedMsg && isset($parsedMsg['deprecation'])) { - $this->message = $parsedMsg['deprecation']; - $this->originClass = $parsedMsg['class']; - $this->originMethod = $parsedMsg['method']; - if (isset($parsedMsg['files_stack'])) { - $this->originalFilesStack = $parsedMsg['files_stack']; - } - // If the deprecation has been triggered via - // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest() - // then we need to use the serialized information to determine - // if the error has been triggered from vendor code. - if (isset($parsedMsg['triggering_file'])) { - $this->triggeringFile = $parsedMsg['triggering_file']; - } + if (!isset($line['object']) && !isset($line['class'])) { + return; + } - return; + set_error_handler(function () {}); + $parsedMsg = unserialize($this->message); + restore_error_handler(); + if ($parsedMsg && isset($parsedMsg['deprecation'])) { + $this->message = $parsedMsg['deprecation']; + $this->originClass = $parsedMsg['class']; + $this->originMethod = $parsedMsg['method']; + if (isset($parsedMsg['files_stack'])) { + $this->originalFilesStack = $parsedMsg['files_stack']; + } + // If the deprecation has been triggered via + // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest() + // then we need to use the serialized information to determine + // if the error has been triggered from vendor code. + if (isset($parsedMsg['triggering_file'])) { + $this->triggeringFile = $parsedMsg['triggering_file']; } - if (!isset($line['class'], $trace[$i - 2]['function']) || 0 !== strpos($line['class'], SymfonyTestsListenerFor::class)) { - $this->originClass = isset($line['object']) ? \get_class($line['object']) : $line['class']; - $this->originMethod = $line['function']; + return; + } - return; - } + if (!isset($line['class'], $trace[$i - 2]['function']) || 0 !== strpos($line['class'], SymfonyTestsListenerFor::class)) { + $this->originClass = isset($line['object']) ? \get_class($line['object']) : $line['class']; + $this->originMethod = $line['function']; + + return; + } - if ('trigger_error' !== $trace[$i - 2]['function'] || isset($trace[$i - 2]['class'])) { - $this->originClass = \get_class($line['args'][0]); - $this->originMethod = $line['args'][0]->getName(); + $test = isset($line['args'][0]) ? $line['args'][0] : null; - return; - } + if (($test instanceof TestCase || $test instanceof TestSuite) && ('trigger_error' !== $trace[$i - 2]['function'] || isset($trace[$i - 2]['class']))) { + $this->originClass = \get_class($test); + $this->originMethod = $test->getName(); + + return; } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_deprecation.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_deprecation.phpt new file mode 100644 index 0000000000000..a6b0133af93ed --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_deprecation.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test that a deprecation from the DebugClassLoader triggered by an app class extending a vendor one is considered direct. +--FILE-- + +--EXPECTF-- +Remaining direct deprecation notices (1) + + 1x: The "App\Services\ExtendsDeprecatedFromVendor" class extends "fcy\lib\DeprecatedClass" that is deprecated. + 1x in DebugClassLoader::loadClass from Symfony\Component\ErrorHandler diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/ExtendsDeprecatedFromVendor.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/ExtendsDeprecatedFromVendor.php new file mode 100644 index 0000000000000..b4305e0d08a55 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/ExtendsDeprecatedFromVendor.php @@ -0,0 +1,9 @@ +getTextTemplate(), $message->getHtmlTemplate()])); + $currentRenderingKey = $this->getFingerPrint($message); if ($previousRenderingKey === $currentRenderingKey) { return; } @@ -77,6 +77,23 @@ public function render(Message $message): void $message->context($message->getContext() + [__CLASS__ => $currentRenderingKey]); } + private function getFingerPrint(TemplatedEmail $message): string + { + $messageContext = $message->getContext(); + unset($messageContext[__CLASS__]); + + $payload = [$messageContext, $message->getTextTemplate(), $message->getHtmlTemplate()]; + try { + $serialized = serialize($payload); + } catch (\Exception $e) { + // Serialization of 'Closure' is not allowed + // Happens when context contain a closure, in that case, we assume that context always change. + $serialized = random_bytes(8); + } + + return md5($serialized); + } + private function convertHtmlToText(string $html): string { if (null !== $this->converter) { diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig index 86e2715488338..c990d81370f3c 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig @@ -134,9 +134,15 @@ {% endblock %} {% block form_widget_simple -%} - {% if type is not defined or type != 'hidden' %} - {%- set attr = attr|merge({class: (attr.class|default('') ~ (type|default('') == 'file' ? ' custom-file-input' : ' form-control'))|trim}) -%} - {% endif %} + {%- if type is not defined or type != 'hidden' -%} + {%- set className = ' form-control' -%} + {%- if type|default('') == 'file' -%} + {%- set className = ' custom-file-input' -%} + {%- elseif type|default('') == 'range' -%} + {%- set className = ' form-control-range' -%} + {%- endif -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ className)|trim}) -%} + {%- endif -%} {%- if type is defined and (type == 'range' or type == 'color') %} {# Attribute "required" is not supported #} {%- set required = false -%} @@ -144,12 +150,12 @@ {{- parent() -}} {%- endblock form_widget_simple %} -{%- block widget_attributes -%} - {%- if not valid %} +{% block widget_attributes -%} + {%- if not valid -%} {% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) %} - {% endif -%} + {%- endif -%} {{ parent() }} -{%- endblock widget_attributes -%} +{%- endblock widget_attributes %} {% block button_widget -%} {%- set attr = attr|merge({class: (attr.class|default('btn-secondary') ~ ' btn')|trim}) -%} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php index d786434614bd4..4c92d628850e1 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\Extension\Core\Type\RadioType; +use Symfony\Component\Form\Extension\Core\Type\RangeType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormError; @@ -1227,6 +1228,41 @@ public function testPercentCustomSymbol() [contains(.., "‱")] ] ] +' + ); + } + + public function testRange() + { + $form = $this->factory->createNamed('name', RangeType::class, 42, ['attr' => ['min' => 5]]); + + $this->assertWidgetMatchesXpath( + $form->createView(), + ['attr' => ['class' => 'my&class']], +'/input + [@type="range"] + [@name="name"] + [@value="42"] + [@min="5"] + [@class="my&class form-control-range"] +' + ); + } + + public function testRangeWithMinMaxValues() + { + $form = $this->factory->createNamed('name', RangeType::class, 42, ['attr' => ['min' => 5, 'max' => 57]]); + + $this->assertWidgetMatchesXpath( + $form->createView(), + ['attr' => ['class' => 'my&class']], +'/input + [@type="range"] + [@name="name"] + [@value="42"] + [@min="5"] + [@max="57"] + [@class="my&class form-control-range"] ' ); } diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php index f13ab213c3c4f..8ff343b684b5e 100644 --- a/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Mime/BodyRendererTest.php @@ -100,6 +100,27 @@ public function testRenderedOnce() $this->assertEquals('reset', $email->getTextBody()); } + public function testRenderedOnceUnserializableContext() + { + $twig = new Environment(new ArrayLoader([ + 'text' => 'Text', + ])); + $renderer = new BodyRenderer($twig); + $email = (new TemplatedEmail()) + ->to('fabien@symfony.com') + ->from('helene@symfony.com') + ; + $email->textTemplate('text'); + $email->context([ + 'foo' => static function () { + return 'bar'; + }, + ]); + + $renderer->render($email); + $this->assertEquals('Text', $email->getTextBody()); + } + private function prepareEmail(?string $text, ?string $html, array $context = []): TemplatedEmail { $twig = new Environment(new ArrayLoader([ diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index f167ccd927f43..8012df7ae6d56 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -47,9 +47,9 @@ "symfony/expression-language": "^4.4|^5.0", "symfony/web-link": "^4.4|^5.0", "symfony/workflow": "^5.2", - "twig/cssinliner-extra": "^2.12", - "twig/inky-extra": "^2.12", - "twig/markdown-extra": "^2.12" + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" }, "conflict": { "phpdocumentor/reflection-docblock": "<3.2.2", diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php index 279ef5062faaa..be5f2474448ac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php @@ -113,7 +113,9 @@ private static function formatFileSize(string $path): string } else { $size = 0; foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS)) as $file) { - $size += $file->getSize(); + if ($file->isReadable()) { + $size += $file->getSize(); + } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index c6cd60cc32fb1..00892d8847951 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -1997,7 +1997,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con if (!$container->getParameter('kernel.debug')) { $propertyAccessDefinition->setFactory([PropertyAccessor::class, 'createCache']); - $propertyAccessDefinition->setArguments([null, 0, $version, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)]); + $propertyAccessDefinition->setArguments(['', 0, $version, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)]); $propertyAccessDefinition->addTag('cache.pool', ['clearer' => 'cache.system_clearer']); $propertyAccessDefinition->addTag('monolog.logger', ['channel' => 'cache']); } else { diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index 35ea73c235771..45b2ca785603e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -18,6 +18,7 @@ use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\HttpKernel\HttpCache\StoreInterface; use Symfony\Component\HttpKernel\HttpCache\SurrogateInterface; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelInterface; /** @@ -62,6 +63,15 @@ public function __construct(KernelInterface $kernel, $cache = null, SurrogateInt parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge($this->options, $this->getOptions())); } + public function handle(Request $request, int $type = HttpKernelInterface::MASTER_REQUEST, bool $catch = true) + { + if ($this->kernel->getContainer()->getParameter('kernel.http_method_override')) { + Request::enableHttpMethodParameterOverride(); + } + + return parent::handle($request, $type, $catch); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php index 40381e34aa310..dff1eea251041 100644 --- a/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php +++ b/src/Symfony/Bundle/FrameworkBundle/KernelBrowser.php @@ -120,7 +120,7 @@ public function loginUser($user, string $firewallContext = 'main'): self throw new \LogicException(sprintf('The first argument of "%s" must be instance of "%s", "%s" provided.', __METHOD__, UserInterface::class, \is_object($user) ? \get_class($user) : \gettype($user))); } - $token = new TestBrowserToken($user->getRoles(), $user); + $token = new TestBrowserToken($user->getRoles(), $user, $firewallContext); $token->setAuthenticated(true); $session = $this->getContainer()->get('session'); $session->set('_security_'.$firewallContext, serialize($token)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php b/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php index 08f7b107d03a4..7580743f6d5cb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/TestBrowserToken.php @@ -21,17 +21,38 @@ */ class TestBrowserToken extends AbstractToken { - public function __construct(array $roles = [], UserInterface $user = null) + private $firewallName; + + public function __construct(array $roles = [], UserInterface $user = null, string $firewallName = 'main') { parent::__construct($roles); if (null !== $user) { $this->setUser($user); } + + $this->firewallName = $firewallName; + } + + public function getFirewallName(): string + { + return $this->firewallName; } public function getCredentials() { return null; } + + public function __serialize(): array + { + return [$this->firewallName, parent::__serialize()]; + } + + public function __unserialize(array $data): void + { + [$this->firewallName, $parentData] = $data; + + parent::__unserialize($parentData); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/AboutCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/AboutCommandTest.php new file mode 100644 index 0000000000000..8a1fcf93caabd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/AboutCommandTest.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command\AboutCommand; + +use Symfony\Bundle\FrameworkBundle\Command\AboutCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\FrameworkBundle\Tests\Command\AboutCommand\Fixture\TestAppKernel; +use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Filesystem; + +class AboutCommandTest extends TestCase +{ + /** @var Filesystem */ + private $fs; + + protected function setUp(): void + { + $this->fs = new Filesystem(); + } + + public function testAboutWithReadableFiles() + { + $kernel = new TestAppKernel('test', true); + $this->fs->mkdir($kernel->getProjectDir()); + + $this->fs->dumpFile($kernel->getCacheDir().'/readable_file', 'The file content.'); + $this->fs->chmod($kernel->getCacheDir().'/readable_file', 0777); + + $tester = $this->createCommandTester($kernel); + $ret = $tester->execute([]); + + $this->assertSame(0, $ret); + $this->assertStringContainsString('Cache directory', $tester->getDisplay()); + $this->assertStringContainsString('Log directory', $tester->getDisplay()); + + $this->fs->chmod($kernel->getCacheDir().'/readable_file', 0777); + + try { + $this->fs->remove($kernel->getProjectDir()); + } catch (IOException $e) { + } + } + + public function testAboutWithUnreadableFiles() + { + $kernel = new TestAppKernel('test', true); + $this->fs->mkdir($kernel->getProjectDir()); + + // skip test on Windows; PHP can't easily set file as unreadable on Windows + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test cannot run on Windows.'); + } + + $this->fs->dumpFile($kernel->getCacheDir().'/unreadable_file', 'The file content.'); + $this->fs->chmod($kernel->getCacheDir().'/unreadable_file', 0222); + + $tester = $this->createCommandTester($kernel); + $ret = $tester->execute([]); + + $this->assertSame(0, $ret); + $this->assertStringContainsString('Cache directory', $tester->getDisplay()); + $this->assertStringContainsString('Log directory', $tester->getDisplay()); + + $this->fs->chmod($kernel->getCacheDir().'/unreadable_file', 0777); + + try { + $this->fs->remove($kernel->getProjectDir()); + } catch (IOException $e) { + } + } + + private function createCommandTester(TestAppKernel $kernel): CommandTester + { + $application = new Application($kernel); + $application->add(new AboutCommand()); + + return new CommandTester($application->find('about')); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/Fixture/TestAppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/Fixture/TestAppKernel.php new file mode 100644 index 0000000000000..c15bf83cb1cf8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/Fixture/TestAppKernel.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command\AboutCommand\Fixture; + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Kernel; + +class TestAppKernel extends Kernel +{ + public function registerBundles(): iterable + { + return [ + new FrameworkBundle(), + ]; + } + + public function getProjectDir(): string + { + return __DIR__.'/test'; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + } + + protected function build(ContainerBuilder $container) + { + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 960eda35d0b62..5cae48bd6ecc2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -1696,7 +1696,7 @@ protected function createContainerFromFile($file, $data = [], $resetCompilerPass $container->getCompilerPassConfig()->setAfterRemovingPasses([]); } $container->getCompilerPassConfig()->setBeforeOptimizationPasses([new LoggerPass()]); - $container->getCompilerPassConfig()->setBeforeRemovingPasses([new AddConstraintValidatorsPass(), new TranslatorPass('translator.default', 'translation.reader')]); + $container->getCompilerPassConfig()->setBeforeRemovingPasses([new AddConstraintValidatorsPass(), new TranslatorPass()]); $container->getCompilerPassConfig()->setAfterRemovingPasses([new AddAnnotationsCachedReaderPass()]); if (!$compile) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/bundles.php index 15ff182c6fed5..2e46e896bca7c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/bundles.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/bundles.php @@ -14,5 +14,4 @@ return [ new FrameworkBundle(), - new TestBundle(), ]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/TestBrowserTokenTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/TestBrowserTokenTest.php new file mode 100644 index 0000000000000..8c5387590a43a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/TestBrowserTokenTest.php @@ -0,0 +1,16 @@ +assertSame('main', $token->getFirewallName()); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php index 3afddb3d35f3b..0b99f281e9a94 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginLinkFactory.php @@ -31,7 +31,7 @@ class LoginLinkFactory extends AbstractFactory implements AuthenticatorFactoryIn public function addConfiguration(NodeDefinition $node) { /** @var NodeBuilder $builder */ - $builder = $node->children(); + $builder = $node->fixXmlConfig('signature_property', 'signature_properties')->children(); $builder ->scalarNode('check_route') @@ -98,6 +98,10 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal if (null !== $config['max_uses'] && !isset($config['used_link_cache'])) { $config['used_link_cache'] = 'security.authenticator.cache.expired_links'; + $defaultCacheDefinition = $container->getDefinition($config['used_link_cache']); + if (!$defaultCacheDefinition->hasTag('cache.pool')) { + $defaultCacheDefinition->addTag('cache.pool'); + } } $expiredStorageId = null; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php index c0aa37ec88712..1ff09a48ac5b9 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php @@ -64,7 +64,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal } if (!class_exists(RateLimiterFactory::class)) { - throw new \LogicException('Login throttling requires symfony/rate-limiter to be installed and enabled.'); + throw new \LogicException('Login throttling requires the Rate Limiter component. Try running "composer require symfony/rate-limiter".'); } if (!isset($config['limiter'])) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index 884c7b5721f20..27ec6ff9e0ac6 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -108,7 +108,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal $container ->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me')) ->replaceArgument(0, new Reference($rememberMeServicesId)) - ->replaceArgument(3, array_intersect_key($config, $this->options)) + ->replaceArgument(3, $container->getDefinition($rememberMeServicesId)->getArgument(3)) ; foreach ($container->findTaggedServiceIds('security.remember_me_aware') as $serviceId => $attributes) { @@ -201,7 +201,12 @@ private function createRememberMeServices(ContainerBuilder $container, string $i } // remember-me options - $rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options)); + $mergedOptions = array_intersect_key($config, $this->options); + if ('auto' === $mergedOptions['secure']) { + $mergedOptions['secure'] = null; + } + + $rememberMeServices->replaceArgument(3, $mergedOptions); if ($config['user_providers']) { $userProviders = []; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd index 8ff0d5e46da0d..509ac0b0534f7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/schema/security-1.0.xsd @@ -23,6 +23,7 @@ + @@ -141,6 +142,7 @@ + @@ -160,6 +162,7 @@ + @@ -231,6 +234,7 @@ + @@ -283,6 +287,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php index 2efb089262cfe..2248b5e8eeb7d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_authenticator_login_link.php @@ -51,7 +51,6 @@ ->set('security.authenticator.cache.expired_links') ->parent('cache.app') ->private() - ->tag('cache.pool') ->set('security.authenticator.firewall_aware_login_link_handler', FirewallAwareLoginLinkHandler::class) ->args([ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index 072e33aca6f4d..47d3033a25582 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; abstract class CompleteConfigurationTest extends TestCase { @@ -28,6 +29,38 @@ abstract protected function getLoader(ContainerBuilder $container); abstract protected function getFileExtension(); + public function testAuthenticatorManager() + { + $container = $this->getContainer('authenticator_manager'); + + $this->assertEquals(AuthenticatorManager::class, $container->getDefinition('security.authenticator.manager.main')->getClass()); + + // login link + $expiredStorage = $container->getDefinition($expiredStorageId = 'security.authenticator.expired_login_link_storage.main'); + $this->assertEquals('cache.redis', (string) $expiredStorage->getArgument(0)); + $this->assertEquals(3600, (string) $expiredStorage->getArgument(1)); + + $linker = $container->getDefinition($linkerId = 'security.authenticator.login_link_handler.main'); + $this->assertEquals(['id', 'email'], $linker->getArgument(3)); + $this->assertEquals([ + 'route_name' => 'login_check', + 'lifetime' => 3600, + 'max_uses' => 1, + ], $linker->getArgument(5)); + $this->assertEquals($expiredStorageId, (string) $linker->getArgument(6)); + + $authenticator = $container->getDefinition('security.authenticator.login_link.main'); + $this->assertEquals($linkerId, (string) $authenticator->getArgument(0)); + $this->assertEquals([ + 'check_route' => 'login_check', + 'check_post_only' => true, + ], $authenticator->getArgument(4)); + + // login throttling + $listener = $container->getDefinition('security.listener.login_throttling.main'); + $this->assertEquals('app.rate_limiter', (string) $listener->getArgument(1)); + } + public function testRolesHierarchy() { $container = $this->getContainer('container1'); @@ -648,6 +681,7 @@ protected function getContainer($file) $container->setParameter('kernel.debug', false); $container->setParameter('request_listener.http_port', 80); $container->setParameter('request_listener.https_port', 443); + $container->register('cache.app', \stdClass::class); $security = new SecurityExtension(); $container->registerExtension($security); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php new file mode 100644 index 0000000000000..31a37fe2103f9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/authenticator_manager.php @@ -0,0 +1,20 @@ +loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'firewalls' => [ + 'main' => [ + 'login_link' => [ + 'check_route' => 'login_check', + 'check_post_only' => true, + 'signature_properties' => ['id', 'email'], + 'max_uses' => 1, + 'lifetime' => 3600, + 'used_link_cache' => 'cache.redis', + ], + 'login_throttling' => [ + 'limiter' => 'app.rate_limiter', + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml new file mode 100644 index 0000000000000..2a3b643a6e905 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/authenticator_manager.xml @@ -0,0 +1,24 @@ + + + + + + + id + email + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/authenticator_manager.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/authenticator_manager.yml new file mode 100644 index 0000000000000..8ff11698ae772 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/authenticator_manager.yml @@ -0,0 +1,13 @@ +security: + enable_authenticator_manager: true + firewalls: + main: + login_link: + check_route: login_check + check_post_only: true + signature_properties: [id, email] + max_uses: 1 + lifetime: 3600 + used_link_cache: 'cache.redis' + login_throttling: + limiter: 'app.rate_limiter' diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php new file mode 100644 index 0000000000000..6bfa1ed438732 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php @@ -0,0 +1,33 @@ +createClient(['test_case' => 'RememberMeCookie', 'root_config' => 'config.yml']); + + $client->request('POST', '/login', [ + '_username' => 'test', + '_password' => 'test', + ], [], [ + 'HTTPS' => (int) $https, + ]); + + $cookies = $client->getResponse()->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY); + + $this->assertEquals($expectedSecureFlag, $cookies['']['/']['REMEMBERME']->isSecure()); + } + + public function getSessionRememberMeSecureCookieFlagAutoHttpsMap() + { + return [ + [true, true], + [false, false], + ]; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/bundles.php new file mode 100644 index 0000000000000..8d4a02497947a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/bundles.php @@ -0,0 +1,9 @@ + a:hover { display: block; text-decoration: none; + background-color: transparent; color: inherit; } @@ -243,6 +244,7 @@ div.sf-toolbar .sf-toolbar-block a:hover { padding: 0 10px; } .sf-toolbar-block-request .sf-toolbar-info-piece a { + background-color: transparent; text-decoration: none; } .sf-toolbar-block-request .sf-toolbar-info-piece a:hover { diff --git a/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php b/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php index f704bbfe0e49f..6aa94c2c63383 100644 --- a/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php +++ b/src/Symfony/Component/Cache/Tests/DataCollector/CacheDataCollectorTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Cache\Tests\Marshaller; +namespace Symfony\Component\Cache\Tests\DataCollector; use PHPUnit\Framework\TestCase; use Symfony\Component\Cache\Adapter\TraceableAdapter; diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index 65f54f8013dd1..66e8c37181858 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -395,9 +395,9 @@ public function addArgument(string $name, int $mode = null, string $description /** * Adds an option. * - * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts - * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants - * @param string|string[]|int|bool|null $default The default value (must be null for InputOption::VALUE_NONE) + * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants + * @param string|string[]|bool|null $default The default value (must be null for InputOption::VALUE_NONE) * * @throws InvalidArgumentException If option mode is invalid or incompatible * diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index f82dd286fc3b5..acec994db83c4 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\String\UnicodeString; /** * Helper is the base class for all helper classes. @@ -45,7 +46,11 @@ public function getHelperSet() */ public static function strlen(?string $string) { - $string = (string) $string; + $string ?? $string = ''; + + if (preg_match('//u', $string)) { + return (new UnicodeString($string))->width(false); + } if (false === $encoding = mb_detect_encoding($string, null, true)) { return \strlen($string); @@ -59,9 +64,9 @@ public static function strlen(?string $string) * * @return string The string subset */ - public static function substr(string $string, int $from, int $length = null) + public static function substr(?string $string, int $from, int $length = null) { - $string = (string) $string; + $string ?? $string = ''; if (false === $encoding = mb_detect_encoding($string, null, true)) { return substr($string, $from, $length); @@ -116,17 +121,23 @@ public static function formatMemory(int $memory) return sprintf('%d B', $memory); } - public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string) + public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, ?string $string) { - return self::strlen(self::removeDecoration($formatter, $string)); + $string = self::removeDecoration($formatter, $string); + + if (preg_match('//u', $string)) { + return (new UnicodeString($string))->width(true); + } + + return self::strlen($string); } - public static function removeDecoration(OutputFormatterInterface $formatter, $string) + public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string) { $isDecorated = $formatter->isDecorated(); $formatter->setDecorated(false); // remove <...> formatting - $string = $formatter->format($string); + $string = $formatter->format($string ?? ''); // remove already formatted characters $string = preg_replace("/\033\[[^m]*m/", '', $string); $formatter->setDecorated($isDecorated); diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php index a3aa4809779f4..61c471424ad4a 100644 --- a/src/Symfony/Component/Console/Helper/ProgressBar.php +++ b/src/Symfony/Component/Console/Helper/ProgressBar.php @@ -462,8 +462,15 @@ private function overwrite(string $message): void if ($this->overwrite) { if (null !== $this->previousMessage) { if ($this->output instanceof ConsoleSectionOutput) { - $lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1; - $this->output->clear($lines); + $messageLines = explode("\n", $message); + $lineCount = \count($messageLines); + foreach ($messageLines as $messageLine) { + $messageLineLength = Helper::strlenWithoutDecoration($this->output->getFormatter(), $messageLine); + if ($messageLineLength > $this->terminal->getWidth()) { + $lineCount += floor($messageLineLength / $this->terminal->getWidth()); + } + } + $this->output->clear($lineCount); } else { if ($this->formatLineCount > 0) { $this->cursor->moveUp($this->formatLineCount); diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index c0fb5461fc660..5bf8186b8fbf8 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -311,7 +311,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)))); $output->write($remainingCharacters); $fullChoice .= $remainingCharacters; - $i = self::strlen($fullChoice); + $i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding); $matches = array_filter( $autocomplete($ret), diff --git a/src/Symfony/Component/Console/Input/ArrayInput.php b/src/Symfony/Component/Console/Input/ArrayInput.php index 66f0bedc3626b..5c1e2f63ae58f 100644 --- a/src/Symfony/Component/Console/Input/ArrayInput.php +++ b/src/Symfony/Component/Console/Input/ArrayInput.php @@ -108,12 +108,13 @@ public function __toString() $params = []; foreach ($this->parameters as $param => $val) { if ($param && \is_string($param) && '-' === $param[0]) { + $glue = ('-' === $param[1]) ? '=' : ' '; if (\is_array($val)) { foreach ($val as $v) { - $params[] = $param.('' != $v ? '='.$this->escapeToken($v) : ''); + $params[] = $param.('' != $v ? $glue.$this->escapeToken($v) : ''); } } else { - $params[] = $param.('' != $val ? '='.$this->escapeToken($val) : ''); + $params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : ''); } } else { $params[] = \is_array($val) ? implode(' ', array_map([$this, 'escapeToken'], $val)) : $this->escapeToken($val); diff --git a/src/Symfony/Component/Console/Input/InputOption.php b/src/Symfony/Component/Console/Input/InputOption.php index 66f857a6c0d3b..a8e956db55b19 100644 --- a/src/Symfony/Component/Console/Input/InputOption.php +++ b/src/Symfony/Component/Console/Input/InputOption.php @@ -33,11 +33,11 @@ class InputOption private $description; /** - * @param string $name The option name - * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts - * @param int|null $mode The option mode: One of the VALUE_* constants - * @param string $description A description text - * @param string|string[]|int|bool|null $default The default value (must be null for self::VALUE_NONE) + * @param string $name The option name + * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the VALUE_* constants + * @param string $description A description text + * @param string|string[]|bool|null $default The default value (must be null for self::VALUE_NONE) * * @throws InvalidArgumentException If option mode is invalid or incompatible */ @@ -149,7 +149,7 @@ public function isArray() /** * Sets the default value. * - * @param string|string[]|int|bool|null $default The default value + * @param string|string[]|bool|null $default The default value * * @throws LogicException When incorrect default value is given */ @@ -173,7 +173,7 @@ public function setDefault($default = null) /** * Returns the default value. * - * @return string|string[]|int|bool|null The default value + * @return string|string[]|bool|null The default value */ public function getDefault() { diff --git a/src/Symfony/Component/Console/Style/SymfonyStyle.php b/src/Symfony/Component/Console/Style/SymfonyStyle.php index a1b019ca0a781..075fe6621cc1f 100644 --- a/src/Symfony/Component/Console/Style/SymfonyStyle.php +++ b/src/Symfony/Component/Console/Style/SymfonyStyle.php @@ -476,7 +476,12 @@ private function createBlock(iterable $messages, string $type = null, string $st $message = OutputFormatter::escape($message); } - $lines = array_merge($lines, explode(\PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, \PHP_EOL, true))); + $decorationLength = Helper::strlen($message) - Helper::strlenWithoutDecoration($this->getFormatter(), $message); + $messageLineLength = min($this->lineLength - $prefixLength - $indentLength + $decorationLength, $this->lineLength); + $messageLines = explode(\PHP_EOL, wordwrap($message, $messageLineLength, \PHP_EOL, true)); + foreach ($messageLines as $messageLine) { + $lines[] = $messageLine; + } if (\count($messages) > 1 && $key < \count($messages) - 1) { $lines[] = ''; @@ -496,7 +501,7 @@ private function createBlock(iterable $messages, string $type = null, string $st } $line = $prefix.$line; - $line .= str_repeat(' ', $this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line)); + $line .= str_repeat(' ', max($this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line), 0)); if ($style) { $line = sprintf('<%s>%s', $style, $line); diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_21.php b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_21.php new file mode 100644 index 0000000000000..8460e7ececf37 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/command/command_21.php @@ -0,0 +1,13 @@ +success('Lorem ipsum dolor sit amet'); + $output->success('Lorem ipsum dolor sit amet with one emoji 🎉'); + $output->success('Lorem ipsum dolor sit amet with so many of them 👩‍🌾👩‍🌾👩‍🌾👩‍🌾👩‍🌾'); +}; diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt index 0f3704b7482ea..ea8e4351eafa5 100644 --- a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt +++ b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_13.txt @@ -1,7 +1,7 @@ - // Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et  - // dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea  - // commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla  - // pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim - // id est laborum + // Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore  + // magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo  + // consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla  + // pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + // est laborum diff --git a/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_21.txt b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_21.txt new file mode 100644 index 0000000000000..aee3c4a89c2e7 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/Style/SymfonyStyle/output/output_21.txt @@ -0,0 +1,7 @@ + + [OK] Lorem ipsum dolor sit amet + + [OK] Lorem ipsum dolor sit amet with one emoji 🎉 + + [OK] Lorem ipsum dolor sit amet with so many of them 👩‍🌾👩‍🌾👩‍🌾👩‍🌾👩‍🌾 + diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php index 24c69789c2929..8baf5a6d824cc 100644 --- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php @@ -343,6 +343,31 @@ public function testOverwriteWithSectionOutput() ); } + public function testOverwriteWithAnsiSectionOutput() + { + // output has 43 visible characters plus 2 invisible ANSI characters + putenv('COLUMNS=43'); + $sections = []; + $stream = $this->getOutputStream(true); + $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + + $bar = new ProgressBar($output, 50, 0); + $bar->setFormat(" \033[44;37m%current%/%max%\033[0m [%bar%] %percent:3s%%"); + $bar->start(); + $bar->display(); + $bar->advance(); + $bar->advance(); + + rewind($output->getStream()); + $this->assertSame( + " \033[44;37m 0/50\033[0m [>---------------------------] 0%".\PHP_EOL. + "\x1b[1A\x1b[0J"." \033[44;37m 1/50\033[0m [>---------------------------] 2%".\PHP_EOL. + "\x1b[1A\x1b[0J"." \033[44;37m 2/50\033[0m [=>--------------------------] 4%".\PHP_EOL, + stream_get_contents($output->getStream()) + ); + putenv('COLUMNS=120'); + } + public function testOverwriteMultipleProgressBarsWithSectionOutputs() { $sections = []; @@ -372,6 +397,34 @@ public function testOverwriteMultipleProgressBarsWithSectionOutputs() ); } + public function testOverwriteWithSectionOutputWithNewlinesInMessage() + { + $sections = []; + $stream = $this->getOutputStream(true); + $output = new ConsoleSectionOutput($stream->getStream(), $sections, $stream->getVerbosity(), $stream->isDecorated(), new OutputFormatter()); + + ProgressBar::setFormatDefinition('test', '%current%/%max% [%bar%] %percent:3s%% %message% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'); + + $bar = new ProgressBar($output, 50, 0); + $bar->setFormat('test'); + $bar->start(); + $bar->display(); + $bar->setMessage("Twas brillig, and the slithy toves. Did gyre and gimble in the wabe: All mimsy were the borogoves, And the mome raths outgrabe.\nBeware the Jabberwock, my son! The jaws that bite, the claws that catch! Beware the Jubjub bird, and shun The frumious Bandersnatch!"); + $bar->advance(); + $bar->setMessage("He took his vorpal sword in hand; Long time the manxome foe he sought— So rested he by the Tumtum tree And stood awhile in thought.\nAnd, as in uffish thought he stood, The Jabberwock, with eyes of flame, Came whiffling through the tulgey wood, And burbled as it came!"); + $bar->advance(); + + rewind($output->getStream()); + $this->assertEquals( + ' 0/50 [>] 0% %message% Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.'.\PHP_EOL. + "\x1b[6A\x1b[0J 1/50 [>] 2% Twas brillig, and the slithy toves. Did gyre and gimble in the wabe: All mimsy were the borogoves, And the mome raths outgrabe. +Beware the Jabberwock, my son! The jaws that bite, the claws that catch! Beware the Jubjub bird, and shun The frumious Bandersnatch! Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.".\PHP_EOL. + "\x1b[6A\x1b[0J 2/50 [>] 4% He took his vorpal sword in hand; Long time the manxome foe he sought— So rested he by the Tumtum tree And stood awhile in thought. +And, as in uffish thought he stood, The Jabberwock, with eyes of flame, Came whiffling through the tulgey wood, And burbled as it came! Fruitcake marzipan toffee. Cupcake gummi bears tart dessert ice cream chupa chups cupcake chocolate bar sesame snaps. Croissant halvah cookie jujubes powder macaroon. Fruitcake bear claw bonbon jelly beans oat cake pie muffin Fruitcake marzipan toffee.".\PHP_EOL, + stream_get_contents($output->getStream()) + ); + } + public function testMultipleSectionsWithCustomFormat() { $sections = []; diff --git a/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php b/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php index f3eedb3a2d57f..5777c44b7269a 100644 --- a/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php +++ b/src/Symfony/Component/Console/Tests/Input/ArrayInputTest.php @@ -162,10 +162,10 @@ public function provideInvalidInput() public function testToString() { $input = new ArrayInput(['-f' => null, '-b' => 'bar', '--foo' => 'b a z', '--lala' => null, 'test' => 'Foo', 'test2' => "A\nB'C"]); - $this->assertEquals('-f -b=bar --foo='.escapeshellarg('b a z').' --lala Foo '.escapeshellarg("A\nB'C"), (string) $input); + $this->assertEquals('-f -b bar --foo='.escapeshellarg('b a z').' --lala Foo '.escapeshellarg("A\nB'C"), (string) $input); $input = new ArrayInput(['-b' => ['bval_1', 'bval_2'], '--f' => ['fval_1', 'fval_2']]); - $this->assertSame('-b=bval_1 -b=bval_2 --f=fval_1 --f=fval_2', (string) $input); + $this->assertSame('-b bval_1 -b bval_2 --f=fval_1 --f=fval_2', (string) $input); $input = new ArrayInput(['array_arg' => ['val_1', 'val_2']]); $this->assertSame('val_1 val_2', (string) $input); diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index a941b2d3eb77c..4fe4839179af6 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -416,7 +416,7 @@ private function parseDefinition(string $id, $service, string $file, array $defa trigger_deprecation('symfony/dependency-injection', '5.1', 'Not setting the attribute "version" of the "deprecated" option in "%s" is deprecated.', $file); } - $alias->setDeprecated($deprecation['package'] ?? '', $deprecation['version'] ?? '', $deprecation['message']); + $alias->setDeprecated($deprecation['package'] ?? '', $deprecation['version'] ?? '', $deprecation['message'] ?? ''); } } @@ -485,7 +485,7 @@ private function parseDefinition(string $id, $service, string $file, array $defa trigger_deprecation('symfony/dependency-injection', '5.1', 'Not setting the attribute "version" of the "deprecated" option in "%s" is deprecated.', $file); } - $definition->setDeprecated($deprecation['package'] ?? '', $deprecation['version'] ?? '', $deprecation['message']); + $definition->setDeprecated($deprecation['package'] ?? '', $deprecation['version'] ?? '', $deprecation['message'] ?? ''); } if (isset($service['factory'])) { diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/deprecated_definition_without_message.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/deprecated_definition_without_message.yml new file mode 100644 index 0000000000000..7fe725fa608ff --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/deprecated_definition_without_message.yml @@ -0,0 +1,12 @@ +services: + service_without_deprecation_message: + class: Foo + deprecated: + package: vendor/package + version: 1.1 + + alias_without_deprecation_message: + alias: foobar + deprecated: + package: vendor/package + version: 1.1 diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 1a1dc9444a5f7..8591b9f12891a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -220,6 +220,27 @@ public function testLoadShortSyntax() $this->assertSame(['$a' => 'a', 'App\Foo' => 'foo'], $services['bar_foo']->getArguments()); } + public function testLoadDeprecatedDefinitionWithoutMessageKey() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('deprecated_definition_without_message.yml'); + + $this->assertTrue($container->getDefinition('service_without_deprecation_message')->isDeprecated()); + $deprecation = $container->getDefinition('service_without_deprecation_message')->getDeprecation('service_without_deprecation_message'); + $message = 'The "service_without_deprecation_message" service is deprecated. You should stop using it, as it will be removed in the future.'; + $this->assertSame($message, $deprecation['message']); + $this->assertSame('vendor/package', $deprecation['package']); + $this->assertSame('1.1', $deprecation['version']); + + $this->assertTrue($container->getAlias('alias_without_deprecation_message')->isDeprecated()); + $deprecation = $container->getAlias('alias_without_deprecation_message')->getDeprecation('alias_without_deprecation_message'); + $message = 'The "alias_without_deprecation_message" service alias is deprecated. You should stop using it, as it will be removed in the future.'; + $this->assertSame($message, $deprecation['message']); + $this->assertSame('vendor/package', $deprecation['package']); + $this->assertSame('1.1', $deprecation['version']); + } + public function testDeprecatedAliases() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php index 09643dcb70ab9..2850293096659 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php @@ -352,7 +352,7 @@ private function include(string $name, array $context = []): string extract($context, \EXTR_SKIP); ob_start(); - include file_exists($name) ? $name : __DIR__.'/../Resources/'.$name; + include is_file(\dirname(__DIR__).'/Resources/'.$name) ? \dirname(__DIR__).'/Resources/'.$name : $name; return trim(ob_get_clean()); } diff --git a/src/Symfony/Component/ErrorHandler/Exception/FlattenException.php b/src/Symfony/Component/ErrorHandler/Exception/FlattenException.php index 10cc5c56bf357..817c48d823357 100644 --- a/src/Symfony/Component/ErrorHandler/Exception/FlattenException.php +++ b/src/Symfony/Component/ErrorHandler/Exception/FlattenException.php @@ -24,17 +24,40 @@ */ class FlattenException { + /** @var string */ private $message; + + /** @var int|string */ private $code; + + /** @var self|null */ private $previous; + + /** @var array */ private $trace; + + /** @var string */ private $traceAsString; + + /** @var string */ private $class; + + /** @var int */ private $statusCode; + + /** @var string */ private $statusText; + + /** @var array */ private $headers; + + /** @var string */ private $file; + + /** @var int */ private $line; + + /** @var string|null */ private $asString; /** @@ -108,6 +131,8 @@ public function getStatusCode(): int } /** + * @param int $code + * * @return $this */ public function setStatusCode($code): self @@ -138,6 +163,8 @@ public function getClass(): string } /** + * @param string $class + * * @return $this */ public function setClass($class): self @@ -153,6 +180,8 @@ public function getFile(): string } /** + * @param string $file + * * @return $this */ public function setFile($file): self @@ -168,6 +197,8 @@ public function getLine(): int } /** + * @param int $line + * * @return $this */ public function setLine($line): self @@ -195,6 +226,8 @@ public function getMessage(): string } /** + * @param string $message + * * @return $this */ public function setMessage($message): self @@ -219,6 +252,8 @@ public function getCode() } /** + * @param int|string $code + * * @return $this */ public function setCode($code): self @@ -273,6 +308,10 @@ public function setTraceFromThrowable(\Throwable $throwable): self } /** + * @param array $trace + * @param string|null $file + * @param int|null $line + * * @return $this */ public function setTrace($trace, $file, $line): self diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index f8b150f56434f..4a1350de0b725 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -1741,7 +1741,7 @@ public function testDumpKeepsExistingPermissionsWhenOverwritingAnExistingFile() file_put_contents($filename, 'FOO BAR'); chmod($filename, 0745); - $this->filesystem->dumpFile($filename, 'bar', null); + $this->filesystem->dumpFile($filename, 'bar'); $this->assertFilePermissions(745, $filename); } @@ -1762,7 +1762,7 @@ public function testCopyShouldKeepExecutionPermission() } /** - * Normalize the given path (transform each blackslash into a real directory separator). + * Normalize the given path (transform each forward slash into a real directory separator). */ private function normalize(string $path): string { diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php index 501c26db6bde3..5dd5b498c6896 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php @@ -130,6 +130,10 @@ public function reverseTransform($value) } elseif ($timestamp > 253402214400) { // This timestamp represents UTC midnight of 9999-12-31 to prevent 5+ digit years throw new TransformationFailedException('Years beyond 9999 are not supported.'); + } elseif (false === $timestamp) { + // the value couldn't be parsed but the Intl extension didn't report an error code, this + // could be the case when the Intl polyfill is used which always returns 0 as the error code + throw new TransformationFailedException(sprintf('"%s" could not be parsed as a date.', $value)); } try { diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php index 9325a1aa66c25..3af9809ed1f97 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/IntegerToLocalizedStringTransformer.php @@ -24,12 +24,13 @@ class IntegerToLocalizedStringTransformer extends NumberToLocalizedStringTransfo /** * Constructs a transformer. * - * @param bool $grouping Whether thousands should be grouped - * @param int $roundingMode One of the ROUND_ constants in this class + * @param bool $grouping Whether thousands should be grouped + * @param int $roundingMode One of the ROUND_ constants in this class + * @param string|null $locale locale used for transforming */ - public function __construct(?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_DOWN) + public function __construct(?bool $grouping = false, ?int $roundingMode = \NumberFormatter::ROUND_DOWN, ?string $locale = null) { - parent::__construct(0, $grouping, $roundingMode); + parent::__construct(0, $grouping, $roundingMode, $locale); } /** diff --git a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php index eaea6651dc325..fc30dd57e3f0e 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/ChoiceType.php @@ -159,6 +159,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) $knownValues[$child->getName()] = $value; unset($unknownValues[$value]); continue; + } else { + $knownValues[$child->getName()] = null; } } } else { diff --git a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php index 27e3224d70c28..a1cd058a94834 100644 --- a/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php +++ b/src/Symfony/Component/Form/Extension/Core/Type/IntegerType.php @@ -26,7 +26,7 @@ class IntegerType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->addViewTransformer(new IntegerToLocalizedStringTransformer($options['grouping'], $options['rounding_mode'])); + $builder->addViewTransformer(new IntegerToLocalizedStringTransformer($options['grouping'], $options['rounding_mode'], !$options['grouping'] ? 'en' : null)); } /** diff --git a/src/Symfony/Component/Form/Resources/translations/validators.nn.xlf b/src/Symfony/Component/Form/Resources/translations/validators.nn.xlf index 2f5da23444d0b..9fac1bf34e34f 100644 --- a/src/Symfony/Component/Form/Resources/translations/validators.nn.xlf +++ b/src/Symfony/Component/Form/Resources/translations/validators.nn.xlf @@ -4,7 +4,7 @@ This form should not contain extra fields. - Feltgruppa må ikkje innehalde ekstra felt. + Feltgruppa kan ikkje innehalde ekstra felt. The uploaded file was too large. Please try to upload a smaller file. @@ -14,6 +14,126 @@ The CSRF token is invalid. CSRF-nøkkelen er ikkje gyldig. + + This value is not a valid HTML5 color. + Verdien er ikkje ein gyldig HTML5-farge. + + + Please enter a valid birthdate. + Gje opp ein gyldig fødselsdato. + + + The selected choice is invalid. + Valget du gjorde er ikkje gyldig. + + + The collection is invalid. + Samlinga er ikkje gyldig. + + + Please select a valid color. + Gje opp ein gyldig farge. + + + Please select a valid country. + Gje opp eit gyldig land. + + + Please select a valid currency. + Gje opp ein gyldig valuta. + + + Please choose a valid date interval. + Gje opp eit gyldig datointervall. + + + Please enter a valid date and time. + Gje opp ein gyldig dato og tid. + + + Please enter a valid date. + Gje opp ein gyldig dato. + + + Please select a valid file. + Velg ei gyldig fil. + + + The hidden field is invalid. + Det skjulte feltet er ikkje gyldig. + + + Please enter an integer. + Gje opp eit heiltal. + + + Please select a valid language. + Gje opp eit gyldig språk. + + + Please select a valid locale. + Gje opp eit gyldig locale. + + + Please enter a valid money amount. + Gje opp ein gyldig sum pengar. + + + Please enter a number. + Gje opp eit nummer. + + + The password is invalid. + Passordet er ikkje gyldig. + + + Please enter a percentage value. + Gje opp ein prosentverdi. + + + The values do not match. + Verdiane er ikkje eins. + + + Please enter a valid time. + Gje opp ei gyldig tid. + + + Please select a valid timezone. + Gje opp ei gyldig tidssone. + + + Please enter a valid URL. + Gje opp ein gyldig URL. + + + Please enter a valid search term. + Gje opp gyldige søkjeord. + + + Please provide a valid phone number. + Gje opp eit gyldig telefonnummer. + + + The checkbox has an invalid value. + Sjekkboksen har ein ugyldig verdi. + + + Please enter a valid email address. + Gje opp ei gyldig e-postadresse. + + + Please select a valid option. + Velg eit gyldig vilkår. + + + Please select a valid range. + Velg eit gyldig spenn. + + + Please enter a valid week. + Gje opp ei gyldig veke. + diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php index 3eeee26a7e9d7..b09665084dd6e 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/ChoiceTypeTest.php @@ -1321,6 +1321,20 @@ public function testSubmitSingleExpandedObjectChoices() $this->assertNull($form[4]->getViewData()); } + public function testSubmitSingleExpandedClearMissingFalse() + { + $form = $this->factory->create(self::TESTED_TYPE, 'foo', [ + 'choices' => [ + 'foo label' => 'foo', + 'bar label' => 'bar', + ], + 'expanded' => true, + ]); + $form->submit('bar', false); + + $this->assertSame('bar', $form->getData()); + } + public function testSubmitMultipleExpanded() { $form = $this->factory->create(static::TESTED_TYPE, null, [ diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/Type/IntegerTypeTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/Type/IntegerTypeTest.php index a88eb70c5fe7a..15cf308d990dd 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/Type/IntegerTypeTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/Type/IntegerTypeTest.php @@ -17,13 +17,48 @@ class IntegerTypeTest extends BaseTypeTest { public const TESTED_TYPE = 'Symfony\Component\Form\Extension\Core\Type\IntegerType'; + private $previousLocale; + protected function setUp(): void { IntlTestHelper::requireIntl($this, false); - + $this->previousLocale = \Locale::getDefault(); parent::setUp(); } + protected function tearDown(): void + { + \Locale::setDefault($this->previousLocale); + } + + /** + * @requires extension intl + */ + public function testArabicLocale() + { + \Locale::setDefault('ar'); + + $form = $this->factory->create(static::TESTED_TYPE); + $form->submit('123456'); + + $this->assertSame(123456, $form->getData()); + $this->assertSame('123456', $form->getViewData()); + } + + /** + * @requires extension intl + */ + public function testArabicLocaleNonHtml5() + { + \Locale::setDefault('ar'); + + $form = $this->factory->create(static::TESTED_TYPE, null, ['grouping' => true]); + $form->submit('123456'); + + $this->assertSame(123456, $form->getData()); + $this->assertSame('١٢٣٬٤٥٦', $form->getViewData()); + } + public function testSubmitRejectsFloats() { $form = $this->factory->create(static::TESTED_TYPE); diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index 6f583361f20fe..1c17d8274ab08 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -149,7 +149,7 @@ private function open(): void // Send request and follow redirects when needed $this->handle = $h = fopen($url, 'r', false, $this->context); - self::addResponseHeaders($http_response_header, $this->info, $this->headers, $this->info['debug']); + self::addResponseHeaders(stream_get_meta_data($h)['wrapper_data'], $this->info, $this->headers, $this->info['debug']); $url = $resolver($this->multi, $this->headers['location'][0] ?? null, $this->context); if (null === $url) { diff --git a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php index 988ec53bd9edb..2134e6275af56 100644 --- a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php +++ b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php @@ -289,7 +289,7 @@ public function stream_stat(): array 'uid' => 0, 'gid' => 0, 'rdev' => 0, - 'size' => (int) ($headers['content-length'][0] ?? 0), + 'size' => (int) ($headers['content-length'][0] ?? -1), 'atime' => 0, 'mtime' => strtotime($headers['last-modified'][0] ?? '') ?: 0, 'ctime' => 0, diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index 8c0b7aa330cad..d5296e1fbc552 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -75,6 +75,19 @@ public function testToStream() $this->assertTrue(feof($stream)); } + public function testStreamCopyToStream() + { + $client = $this->getHttpClient(__FUNCTION__); + $response = $client->request('GET', 'http://localhost:8057'); + $h = fopen('php://temp', 'w+'); + stream_copy_to_stream($response->toStream(), $h); + + $this->assertTrue(rewind($h)); + $this->assertSame("{\n \"SER", fread($h, 10)); + $this->assertSame('VER_PROTOCOL', fread($h, 12)); + $this->assertFalse(feof($h)); + } + public function testToStream404() { $client = $this->getHttpClient(__FUNCTION__); diff --git a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php index bc334e1d81af1..12c38153a26f4 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/ConfigDataCollector.php @@ -42,10 +42,17 @@ public function setKernel(KernelInterface $kernel = null) */ public function collect(Request $request, Response $response, \Throwable $exception = null) { + $eom = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE); + $eol = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE); + $this->data = [ 'token' => $response->headers->get('X-Debug-Token'), 'symfony_version' => Kernel::VERSION, - 'symfony_state' => 'unknown', + 'symfony_minor_version' => sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), + 'symfony_lts' => 4 === Kernel::MINOR_VERSION, + 'symfony_state' => $this->determineSymfonyState(), + 'symfony_eom' => $eom->format('F Y'), + 'symfony_eol' => $eol->format('F Y'), 'env' => isset($this->kernel) ? $this->kernel->getEnvironment() : 'n/a', 'debug' => isset($this->kernel) ? $this->kernel->isDebug() : 'n/a', 'php_version' => \PHP_VERSION, @@ -63,14 +70,6 @@ public function collect(Request $request, Response $response, \Throwable $except foreach ($this->kernel->getBundles() as $name => $bundle) { $this->data['bundles'][$name] = new ClassStub(\get_class($bundle)); } - - $this->data['symfony_state'] = $this->determineSymfonyState(); - $this->data['symfony_minor_version'] = sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION); - $this->data['symfony_lts'] = 4 === Kernel::MINOR_VERSION; - $eom = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE); - $eol = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE); - $this->data['symfony_eom'] = $eom->format('F Y'); - $this->data['symfony_eol'] = $eol->format('F Y'); } if (preg_match('~^(\d+(?:\.\d+)*)(.+)?$~', $this->data['php_version'], $matches) && isset($matches[2])) { @@ -220,7 +219,7 @@ public function getEnv() /** * Returns true if the debug is enabled. * - * @return bool true if debug is enabled, false otherwise + * @return bool|string true if debug is enabled, false otherwise or a string if no kernel was set */ public function isDebug() { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php index f8b5d8aa3ac89..d81919919ff46 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/DumpDataCollector.php @@ -178,6 +178,11 @@ public function __wakeup() $charset = array_pop($this->data); $fileLinkFormat = array_pop($this->data); $this->dataCount = \count($this->data); + foreach ($this->data as $dump) { + if (!\is_string($dump['name']) || !\is_string($dump['file']) || !\is_int($dump['line'])) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + } self::__construct($this->stopwatch, \is_string($fileLinkFormat) || $fileLinkFormat instanceof FileLinkFormatter ? $fileLinkFormat : null, \is_string($charset) ? $charset : null); } @@ -252,7 +257,7 @@ public function __destruct() } } - private function doDump(DataDumperInterface $dumper, $data, string $name, string $file, int $line) + private function doDump(DataDumperInterface $dumper, Data $data, string $name, string $file, int $line) { if ($dumper instanceof CliDumper) { $contextDumper = function ($name, $file, $line, $fmt) { diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 24573ec5aba7a..7549ae9b21ac6 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -74,11 +74,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - public const VERSION = '5.2.5'; - public const VERSION_ID = 50205; + public const VERSION = '5.2.6'; + public const VERSION_ID = 50206; public const MAJOR_VERSION = 5; public const MINOR_VERSION = 2; - public const RELEASE_VERSION = 5; + public const RELEASE_VERSION = 6; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '07/2021'; @@ -466,7 +466,7 @@ protected function initializeContainer() if (!flock($lock, $wouldBlock ? \LOCK_SH : \LOCK_EX)) { fclose($lock); $lock = null; - } elseif (!\is_object($this->container = include $cachePath)) { + } elseif (!is_file($cachePath) || !\is_object($this->container = include $cachePath)) { $this->container = null; } elseif (!$oldContainer || \get_class($this->container) !== $oldContainer->name) { flock($lock, \LOCK_UN); diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php index d6753cb31b29b..6fb3ba8f761c4 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/ConfigDataCollectorTest.php @@ -41,6 +41,41 @@ public function testCollect() $this->assertSame(\extension_loaded('xdebug'), $c->hasXDebug()); $this->assertSame(\extension_loaded('Zend OPcache') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN), $c->hasZendOpcache()); $this->assertSame(\extension_loaded('apcu') && filter_var(ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN), $c->hasApcu()); + $this->assertSame(sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); + $this->assertContains($c->getSymfonyState(), ['eol', 'eom', 'dev', 'stable']); + + $eom = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->format('F Y'); + $eol = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE)->format('F Y'); + $this->assertSame($eom, $c->getSymfonyEom()); + $this->assertSame($eol, $c->getSymfonyEol()); + } + + public function testCollectWithoutKernel() + { + $c = new ConfigDataCollector(); + $c->collect(new Request(), new Response()); + + $this->assertSame('n/a', $c->getEnv()); + $this->assertSame('n/a', $c->isDebug()); + $this->assertSame('config', $c->getName()); + $this->assertMatchesRegularExpression('~^'.preg_quote($c->getPhpVersion(), '~').'~', \PHP_VERSION); + $this->assertMatchesRegularExpression('~'.preg_quote((string) $c->getPhpVersionExtra(), '~').'$~', \PHP_VERSION); + $this->assertSame(\PHP_INT_SIZE * 8, $c->getPhpArchitecture()); + $this->assertSame(class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a', $c->getPhpIntlLocale()); + $this->assertSame(date_default_timezone_get(), $c->getPhpTimezone()); + $this->assertSame(Kernel::VERSION, $c->getSymfonyVersion()); + $this->assertSame(4 === Kernel::MINOR_VERSION, $c->isSymfonyLts()); + $this->assertNull($c->getToken()); + $this->assertSame(\extension_loaded('xdebug'), $c->hasXDebug()); + $this->assertSame(\extension_loaded('Zend OPcache') && filter_var(ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN), $c->hasZendOpcache()); + $this->assertSame(\extension_loaded('apcu') && filter_var(ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN), $c->hasApcu()); + $this->assertSame(sprintf('%s.%s', Kernel::MAJOR_VERSION, Kernel::MINOR_VERSION), $c->getSymfonyMinorVersion()); + $this->assertContains($c->getSymfonyState(), ['eol', 'eom', 'dev', 'stable']); + + $eom = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_MAINTENANCE)->format('F Y'); + $eol = \DateTime::createFromFormat('d/m/Y', '01/'.Kernel::END_OF_LIFE)->format('F Y'); + $this->assertSame($eom, $c->getSymfonyEom()); + $this->assertSame($eol, $c->getSymfonyEol()); } } diff --git a/src/Symfony/Component/Inflector/Tests/InflectorTest.php b/src/Symfony/Component/Inflector/Tests/InflectorTest.php index 946e76f4a2776..d54f4ce8206db 100644 --- a/src/Symfony/Component/Inflector/Tests/InflectorTest.php +++ b/src/Symfony/Component/Inflector/Tests/InflectorTest.php @@ -204,6 +204,7 @@ public function pluralizeProvider() ['crisis', 'crises'], ['criteria', 'criterion'], ['cup', 'cups'], + ['coupon', 'coupons'], ['data', 'data'], ['day', 'days'], ['disco', 'discos'], diff --git a/src/Symfony/Component/Inflector/composer.json b/src/Symfony/Component/Inflector/composer.json index 3f8d1eced1493..1162fa4167f86 100644 --- a/src/Symfony/Component/Inflector/composer.json +++ b/src/Symfony/Component/Inflector/composer.json @@ -25,7 +25,7 @@ "require": { "php": ">=7.2.5", "symfony/deprecation-contracts": "^2.1", - "symfony/string": "~5.1.10|^5.2.1" + "symfony/string": "^5.2.6" }, "autoload": { "psr-4": { "Symfony\\Component\\Inflector\\": "" }, diff --git a/src/Symfony/Component/Lock/Tests/Store/StoreFactoryTest.php b/src/Symfony/Component/Lock/Tests/Store/StoreFactoryTest.php index 744d6a0a3894e..e0f70a278543b 100644 --- a/src/Symfony/Component/Lock/Tests/Store/StoreFactoryTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/StoreFactoryTest.php @@ -43,7 +43,7 @@ public function testCreateStore($connection, string $expectedStoreClass) public function validConnections() { if (class_exists(\Redis::class)) { - yield [$this->createMock(\Redis::class), RedisStore::class]; + yield [new \Redis(), RedisStore::class]; } if (class_exists(RedisProxy::class)) { yield [$this->createMock(RedisProxy::class), RedisStore::class]; diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php index 52a7c29f41cb7..5de2bb0199e3e 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiAsyncAwsTransportTest.php @@ -64,8 +64,8 @@ public function testSend() $content = json_decode($options['body'], true); $this->assertSame('Hello!', $content['Content']['Simple']['Subject']['Data']); - $this->assertSame('Saif Eddin ', $content['Destination']['ToAddresses'][0]); - $this->assertSame('Fabien ', $content['FromEmailAddress']); + $this->assertSame('"Saif Eddin" ', $content['Destination']['ToAddresses'][0]); + $this->assertSame('"Fabien" ', $content['FromEmailAddress']); $this->assertSame('Hello There!', $content['Content']['Simple']['Body']['Text']['Data']); $this->assertSame('Hello There!', $content['Content']['Simple']['Body']['Html']['Data']); $this->assertSame(['replyto-1@example.com', 'replyto-2@example.com'], $content['ReplyToAddresses']); diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php index b4dfa191aea0f..e19241d416d4c 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/Tests/Transport/SesApiTransportTest.php @@ -65,8 +65,8 @@ public function testSend() parse_str($options['body'], $content); $this->assertSame('Hello!', $content['Message_Subject_Data']); - $this->assertSame('Saif Eddin ', $content['Destination_ToAddresses_member'][0]); - $this->assertSame('Fabien ', $content['Source']); + $this->assertSame('"Saif Eddin" ', $content['Destination_ToAddresses_member'][0]); + $this->assertSame('"Fabien" ', $content['Source']); $this->assertSame('Hello There!', $content['Message_Body_Text_Data']); $this->assertSame('aws-configuration-set-name', $content['ConfigurationSetName']); diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json b/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json index 6737d1082bae9..5d2ea6bcb5cdb 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/composer.json @@ -17,11 +17,11 @@ ], "require": { "php": ">=7.2.5", + "async-aws/ses": "^1.0", "symfony/deprecation-contracts": "^2.1", - "symfony/mailer": "^4.4.12|^5.1.4" + "symfony/mailer": "^4.4.21|^5.2.6" }, "require-dev": { - "async-aws/ses": "^1.0", "symfony/http-client": "^4.4|^5.0" }, "autoload": { diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php index 67f0c7b935bc8..1ea96bcd8fa55 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php @@ -130,8 +130,8 @@ public function testSend() } $this->assertStringContainsString('Hello!', $content); - $this->assertStringContainsString('Saif Eddin ', $content); - $this->assertStringContainsString('Fabien ', $content); + $this->assertStringContainsString('"Saif Eddin" ', $content); + $this->assertStringContainsString('"Fabien" ', $content); $this->assertStringContainsString('Hello There!', $content); return new MockResponse(json_encode(['id' => 'foobar']), [ diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json b/src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json index 999a6a5fd7b54..728f11761c3da 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": ">=7.2.5", - "symfony/mailer": "^5.1" + "symfony/mailer": "^5.2.6" }, "require-dev": { "symfony/http-client": "^4.4|^5.0" diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php index 1ac55721b9928..760e5a016810a 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php @@ -76,8 +76,8 @@ public function testSend() $this->assertStringContainsStringIgnoringCase('X-Postmark-Server-Token: KEY', $options['headers'][1] ?? $options['request_headers'][1]); $body = json_decode($options['body'], true); - $this->assertSame('Fabien ', $body['From']); - $this->assertSame('Saif Eddin ', $body['To']); + $this->assertSame('"Fabien" ', $body['From']); + $this->assertSame('"Saif Eddin" ', $body['To']); $this->assertSame('Hello!', $body['Subject']); $this->assertSame('Hello There!', $body['TextBody']); diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/composer.json b/src/Symfony/Component/Mailer/Bridge/Postmark/composer.json index 6883a6e0c1a50..4d27122ccafae 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/composer.json +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": ">=7.2.5", - "symfony/mailer": "^5.1" + "symfony/mailer": "^5.2.6" }, "require-dev": { "symfony/http-client": "^4.4|^5.0" diff --git a/src/Symfony/Component/Mailer/composer.json b/src/Symfony/Component/Mailer/composer.json index 673b00167df79..34d864d18d41b 100644 --- a/src/Symfony/Component/Mailer/composer.json +++ b/src/Symfony/Component/Mailer/composer.json @@ -20,7 +20,7 @@ "egulias/email-validator": "^2.1.10|^3", "psr/log": "~1.0", "symfony/event-dispatcher": "^4.4|^5.0", - "symfony/mime": "^5.2", + "symfony/mime": "^5.2.6", "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1|^2" }, diff --git a/src/Symfony/Component/Mime/Address.php b/src/Symfony/Component/Mime/Address.php index 3ef967dd73231..f317e218e11eb 100644 --- a/src/Symfony/Component/Mime/Address.php +++ b/src/Symfony/Component/Mime/Address.php @@ -78,7 +78,16 @@ public function getEncodedAddress(): string public function toString(): string { - return ($n = $this->getName()) ? $n.' <'.$this->getEncodedAddress().'>' : $this->getEncodedAddress(); + return ($n = $this->getEncodedName()) ? $n.' <'.$this->getEncodedAddress().'>' : $this->getEncodedAddress(); + } + + public function getEncodedName(): string + { + if ('' === $this->getName()) { + return ''; + } + + return sprintf('"%s"', preg_replace('/"/u', '\"', $this->getName())); } /** diff --git a/src/Symfony/Component/Mime/Tests/AddressTest.php b/src/Symfony/Component/Mime/Tests/AddressTest.php index d371d0d0a4ca9..a0f2164b5547b 100644 --- a/src/Symfony/Component/Mime/Tests/AddressTest.php +++ b/src/Symfony/Component/Mime/Tests/AddressTest.php @@ -27,7 +27,7 @@ public function testConstructor() $a = new Address('fabien@symfonï.com', 'Fabien'); $this->assertEquals('Fabien', $a->getName()); $this->assertEquals('fabien@symfonï.com', $a->getAddress()); - $this->assertEquals('Fabien ', $a->toString()); + $this->assertEquals('"Fabien" ', $a->toString()); $this->assertEquals('fabien@xn--symfon-nwa.com', $a->getEncodedAddress()); } @@ -170,4 +170,10 @@ public function fromStringProvider() ], ]; } + + public function testEncodeNameIfNameContainsCommas() + { + $address = new Address('fabien@symfony.com', 'Fabien, "Potencier'); + $this->assertSame('"Fabien, \"Potencier" ', $address->toString()); + } } diff --git a/src/Symfony/Component/RateLimiter/RateLimiterFactory.php b/src/Symfony/Component/RateLimiter/RateLimiterFactory.php index 9fdbe474bf3ef..08b588b62cbb6 100644 --- a/src/Symfony/Component/RateLimiter/RateLimiterFactory.php +++ b/src/Symfony/Component/RateLimiter/RateLimiterFactory.php @@ -46,7 +46,7 @@ public function __construct(array $config, StorageInterface $storage, ?LockFacto public function create(?string $key = null): LimiterInterface { - $id = $this->config['id'].$key; + $id = $this->config['id'].'-'.$key; $lock = $this->lockFactory ? $this->lockFactory->createLock($id) : new NoLock(); switch ($this->config['policy']) { diff --git a/src/Symfony/Component/RateLimiter/Tests/Strategy/FixedWindowLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php similarity index 100% rename from src/Symfony/Component/RateLimiter/Tests/Strategy/FixedWindowLimiterTest.php rename to src/Symfony/Component/RateLimiter/Tests/Policy/FixedWindowLimiterTest.php diff --git a/src/Symfony/Component/RateLimiter/Tests/Strategy/SlidingWindowLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php similarity index 100% rename from src/Symfony/Component/RateLimiter/Tests/Strategy/SlidingWindowLimiterTest.php rename to src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowLimiterTest.php diff --git a/src/Symfony/Component/RateLimiter/Tests/Strategy/SlidingWindowTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowTest.php similarity index 100% rename from src/Symfony/Component/RateLimiter/Tests/Strategy/SlidingWindowTest.php rename to src/Symfony/Component/RateLimiter/Tests/Policy/SlidingWindowTest.php diff --git a/src/Symfony/Component/RateLimiter/Tests/Strategy/TokenBucketLimiterTest.php b/src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php similarity index 100% rename from src/Symfony/Component/RateLimiter/Tests/Strategy/TokenBucketLimiterTest.php rename to src/Symfony/Component/RateLimiter/Tests/Policy/TokenBucketLimiterTest.php diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamAfterCommaController.php b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamAfterCommaController.php index 3fdf55f30c373..6ca5aeec66772 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamAfterCommaController.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamAfterCommaController.php @@ -3,14 +3,13 @@ namespace Symfony\Component\Routing\Tests\Fixtures\AttributesFixtures; use Symfony\Component\Routing\Tests\Fixtures\Attributes\FooAttributes; -use Symfony\Component\Security\Core\User\User; #[FooAttributes( foo: [ 'bar' => ['foo','bar'], 'foo' ], - class: User::class + class: \stdClass::class )] class AttributesClassParamAfterCommaController { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamAfterParenthesisController.php b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamAfterParenthesisController.php index 06edcabf6d278..92a6759af65a9 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamAfterParenthesisController.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamAfterParenthesisController.php @@ -3,10 +3,9 @@ namespace Symfony\Component\Routing\Tests\Fixtures\AttributesFixtures; use Symfony\Component\Routing\Tests\Fixtures\Attributes\FooAttributes; -use Symfony\Component\Security\Core\User\User; #[FooAttributes( - class: User::class, + class: \stdClass::class, foo: [ 'bar' => ['foo','bar'], 'foo' diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineAfterCommaController.php b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineAfterCommaController.php index 4b47967f139a3..fc07e916d0c74 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineAfterCommaController.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineAfterCommaController.php @@ -3,9 +3,8 @@ namespace Symfony\Component\Routing\Tests\Fixtures\AttributesFixtures; use Symfony\Component\Routing\Tests\Fixtures\Attributes\FooAttributes; -use Symfony\Component\Security\Core\User\User; -#[FooAttributes(foo: ['bar' => ['foo','bar'],'foo'],class: User::class)] +#[FooAttributes(foo: ['bar' => ['foo','bar'],'foo'],class: \stdClass::class)] class AttributesClassParamInlineAfterCommaController { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineAfterParenthesisController.php b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineAfterParenthesisController.php index 876cdc33f73bc..13f2592edcbcf 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineAfterParenthesisController.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineAfterParenthesisController.php @@ -3,9 +3,8 @@ namespace Symfony\Component\Routing\Tests\Fixtures\AttributesFixtures; use Symfony\Component\Routing\Tests\Fixtures\Attributes\FooAttributes; -use Symfony\Component\Security\Core\User\User; -#[FooAttributes(class: User::class,foo: ['bar' => ['foo','bar'],'foo'])] +#[FooAttributes(class: \stdClass::class,foo: ['bar' => ['foo','bar'],'foo'])] class AttributesClassParamInlineAfterParenthesisController { diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineQuotedAfterCommaController.php b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineQuotedAfterCommaController.php index 471efd305aa62..3bca2bc9669d0 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineQuotedAfterCommaController.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineQuotedAfterCommaController.php @@ -3,7 +3,6 @@ namespace Symfony\Component\Routing\Tests\Fixtures\AttributesFixtures; use Symfony\Component\Routing\Tests\Fixtures\Attributes\FooAttributes; -use Symfony\Component\Security\Core\User\User; #[FooAttributes(foo: ['bar' => ['foo','bar'],'foo'],class: 'Symfony\Component\Security\Core\User\User')] class AttributesClassParamInlineQuotedAfterCommaController diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineQuotedAfterParenthesisController.php b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineQuotedAfterParenthesisController.php index dc0ea7a82096a..31edf3cef98d6 100644 --- a/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineQuotedAfterParenthesisController.php +++ b/src/Symfony/Component/Routing/Tests/Fixtures/AttributesFixtures/AttributesClassParamInlineQuotedAfterParenthesisController.php @@ -3,9 +3,8 @@ namespace Symfony\Component\Routing\Tests\Fixtures\AttributesFixtures; use Symfony\Component\Routing\Tests\Fixtures\Attributes\FooAttributes; -use Symfony\Component\Security\Core\User\User; -#[FooAttributes(class: 'Symfony\Component\Security\Core\User\User',foo: ['bar' => ['foo','bar'],'foo'])] +#[FooAttributes(class: \stdClass::class,foo: ['bar' => ['foo','bar'],'foo'])] class AttributesClassParamInlineQuotedAfterParenthesisController { diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.nn.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.nn.xlf index 27b55e5675cac..89ca44fa88f26 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.nn.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.nn.xlf @@ -36,7 +36,7 @@ No session available, it either timed out or cookies are not enabled. - Ingen sesjon tilgjengeleg. Sesjonen er anten ikkje lenger gyldig, eller informasjonskapslar er ikke skrudd på i nettlesaren. + Ingen sesjon tilgjengeleg. Sesjonen er anten ikkje lenger gyldig, eller informasjonskapslar er ikkje skrudd på i nettlesaren. No token could be found. @@ -56,12 +56,20 @@ Account is disabled. - Brukarkontoen er deaktivert. + Brukarkontoen er sperra. Account is locked. Brukarkontoen er sperra. + + Too many failed login attempts, please try again later. + For mange innloggingsforsøk har feila, prøv igjen seinare. + + + Invalid or expired login link. + Innloggingslenka er ugyldig eller utgjengen. + diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index 4d91656a4246a..15b794c0a7503 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -207,6 +207,7 @@ private function attemptExitUser(Request $request): TokenInterface if (null !== $this->dispatcher && $original->getUser() instanceof UserInterface) { $user = $this->provider->refreshUser($original->getUser()); + $original->setUser($user); $switchEvent = new SwitchUserEvent($request, $user, $original); $this->dispatcher->dispatch($switchEvent, SecurityEvents::SWITCH_USER); $original = $switchEvent->getToken(); diff --git a/src/Symfony/Component/Security/Http/RateLimiter/DefaultLoginRateLimiter.php b/src/Symfony/Component/Security/Http/RateLimiter/DefaultLoginRateLimiter.php index cdf7109cf3ad4..783732e7d8e28 100644 --- a/src/Symfony/Component/Security/Http/RateLimiter/DefaultLoginRateLimiter.php +++ b/src/Symfony/Component/Security/Http/RateLimiter/DefaultLoginRateLimiter.php @@ -41,7 +41,7 @@ protected function getLimiters(Request $request): array { return [ $this->globalFactory->create($request->getClientIp()), - $this->localFactory->create($request->attributes->get(Security::LAST_USERNAME).$request->getClientIp()), + $this->localFactory->create($request->attributes->get(Security::LAST_USERNAME).'-'.$request->getClientIp()), ]; } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/Fixtures/PasswordUpgraderProvider.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/Fixtures/PasswordUpgraderProvider.php index 5836d8893ef7f..d50dbf1be28a9 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/Fixtures/PasswordUpgraderProvider.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/Fixtures/PasswordUpgraderProvider.php @@ -11,9 +11,14 @@ namespace Symfony\Component\Security\Http\Tests\Authenticator\Fixtures; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; -abstract class PasswordUpgraderProvider implements UserProviderInterface, PasswordUpgraderInterface +class PasswordUpgraderProvider extends InMemoryUserProvider implements PasswordUpgraderInterface { + public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + { + } } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php index 71169d988477b..2acb3ed52fa48 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/FormLoginAuthenticatorTest.php @@ -17,8 +17,7 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Security; -use Symfony\Component\Security\Core\User\User; -use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; @@ -37,8 +36,7 @@ class FormLoginAuthenticatorTest extends TestCase protected function setUp(): void { - $this->userProvider = $this->createMock(UserProviderInterface::class); - $this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t')); + $this->userProvider = new InMemoryUserProvider(['test' => ['password' => 's$cr$t']]); $this->successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class); $this->failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class); } @@ -149,8 +147,7 @@ public function testUpgradePassword() $request = Request::create('/login_check', 'POST', ['_username' => 'wouter', '_password' => 's$cr$t']); $request->setSession($this->createSession()); - $this->userProvider = $this->createMock(PasswordUpgraderProvider::class); - $this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t')); + $this->userProvider = new PasswordUpgraderProvider(['test' => ['password' => 's$cr$t']]); $this->setUpAuthenticator(); $passport = $this->authenticator->authenticate($request); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php index 79e914965ab9e..0093dabeeee16 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/HttpBasicAuthenticatorTest.php @@ -6,8 +6,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\User; -use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; @@ -22,7 +22,7 @@ class HttpBasicAuthenticatorTest extends TestCase protected function setUp(): void { - $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->userProvider = new InMemoryUserProvider(); $this->encoderFactory = $this->createMock(EncoderFactoryInterface::class); $this->encoder = $this->createMock(PasswordEncoderInterface::class); $this->encoderFactory @@ -40,16 +40,12 @@ public function testExtractCredentialsAndUserFromRequest() 'PHP_AUTH_PW' => 'ThePassword', ]); - $this->userProvider - ->expects($this->any()) - ->method('loadUserByUsername') - ->with('TheUsername') - ->willReturn($user = new User('TheUsername', 'ThePassword')); + $this->userProvider->createUser($user = new User('TheUsername', 'ThePassword')); $passport = $this->authenticator->authenticate($request); $this->assertEquals('ThePassword', $passport->getBadge(PasswordCredentials::class)->getPassword()); - $this->assertSame($user, $passport->getUser()); + $this->assertTrue($user->isEqualTo($passport->getUser())); } /** @@ -77,8 +73,7 @@ public function testUpgradePassword() 'PHP_AUTH_PW' => 'ThePassword', ]); - $this->userProvider = $this->createMock(PasswordUpgraderProvider::class); - $this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t')); + $this->userProvider = new PasswordUpgraderProvider(['test' => ['password' => 's$cr$t']]); $authenticator = new HttpBasicAuthenticator('test', $this->userProvider); $passport = $authenticator->authenticate($request); diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php index f55c72abff5e5..b51a9fe643158 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RemoteUserAuthenticatorTest.php @@ -14,8 +14,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\User; -use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator; class RemoteUserAuthenticatorTest extends TestCase @@ -23,7 +23,7 @@ class RemoteUserAuthenticatorTest extends TestCase /** * @dataProvider provideAuthenticators */ - public function testSupport(UserProviderInterface $userProvider, RemoteUserAuthenticator $authenticator, $parameterName) + public function testSupport(InMemoryUserProvider $userProvider, RemoteUserAuthenticator $authenticator, $parameterName) { $request = $this->createRequest([$parameterName => 'TheUsername']); @@ -32,7 +32,7 @@ public function testSupport(UserProviderInterface $userProvider, RemoteUserAuthe public function testSupportNoUser() { - $authenticator = new RemoteUserAuthenticator($this->createMock(UserProviderInterface::class), new TokenStorage(), 'main'); + $authenticator = new RemoteUserAuthenticator(new InMemoryUserProvider(), new TokenStorage(), 'main'); $this->assertFalse($authenticator->supports($this->createRequest([]))); } @@ -40,27 +40,24 @@ public function testSupportNoUser() /** * @dataProvider provideAuthenticators */ - public function testAuthenticate(UserProviderInterface $userProvider, RemoteUserAuthenticator $authenticator, $parameterName) + public function testAuthenticate(InMemoryUserProvider $userProvider, RemoteUserAuthenticator $authenticator, $parameterName) { $request = $this->createRequest([$parameterName => 'TheUsername']); $authenticator->supports($request); - $userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->with('TheUsername') - ->willReturn($user = new User('TheUsername', null)); + $userProvider->createUser($user = new User('TheUsername', null)); $passport = $authenticator->authenticate($request); - $this->assertEquals($user, $passport->getUser()); + $this->assertTrue($user->isEqualTo($passport->getUser())); } public function provideAuthenticators() { - $userProvider = $this->createMock(UserProviderInterface::class); + $userProvider = new InMemoryUserProvider(); yield [$userProvider, new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main'), 'REMOTE_USER']; - $userProvider = $this->createMock(UserProviderInterface::class); + $userProvider = new InMemoryUserProvider(); yield [$userProvider, new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main', 'CUSTOM_USER_PARAMETER'), 'CUSTOM_USER_PARAMETER']; } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php index 9f620efd2cfa9..d0322d4759038 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/X509AuthenticatorTest.php @@ -14,8 +14,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\User; -use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\X509Authenticator; class X509AuthenticatorTest extends TestCase @@ -25,7 +25,7 @@ class X509AuthenticatorTest extends TestCase protected function setUp(): void { - $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->userProvider = new InMemoryUserProvider(); $this->authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main'); } @@ -45,10 +45,7 @@ public function testAuthentication($username, $credentials) $request = $this->createRequest($serverVars); $this->assertTrue($this->authenticator->supports($request)); - $this->userProvider->expects($this->any()) - ->method('loadUserByUsername') - ->with($username) - ->willReturn(new User($username, null)); + $this->userProvider->createUser(new User($username, null)); $passport = $this->authenticator->authenticate($request); $this->assertEquals($username, $passport->getUser()->getUsername()); @@ -69,10 +66,7 @@ public function testAuthenticationNoUser($emailAddress, $credentials) $this->assertTrue($this->authenticator->supports($request)); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->with($emailAddress) - ->willReturn(new User($emailAddress, null)); + $this->userProvider->createUser(new User($emailAddress, null)); $passport = $this->authenticator->authenticate($request); $this->assertEquals($emailAddress, $passport->getUser()->getUsername()); @@ -105,10 +99,7 @@ public function testAuthenticationCustomUserKey() ]); $this->assertTrue($authenticator->supports($request)); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->with('TheUser') - ->willReturn(new User('TheUser', null)); + $this->userProvider->createUser(new User('TheUser', null)); $passport = $this->authenticator->authenticate($request); $this->assertEquals('TheUser', $passport->getUser()->getUsername()); @@ -123,10 +114,7 @@ public function testAuthenticationCustomCredentialsKey() ]); $this->assertTrue($authenticator->supports($request)); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->with('cert@example.com') - ->willReturn(new User('cert@example.com', null)); + $this->userProvider->createUser(new User('cert@example.com', null)); $passport = $authenticator->authenticate($request); $this->assertEquals('cert@example.com', $passport->getUser()->getUsername()); diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php index dea08f0186023..285472f037137 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/PasswordMigratingListenerTest.php @@ -16,9 +16,10 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; @@ -36,13 +37,12 @@ class PasswordMigratingListenerTest extends TestCase protected function setUp(): void { - $this->user = $this->createMock(UserInterface::class); - $this->user->expects($this->any())->method('getPassword')->willReturn('old-encoded-password'); + $this->user = new User('test', 'old-encoded-password'); $encoder = $this->createMock(PasswordEncoderInterface::class); $encoder->expects($this->any())->method('needsRehash')->willReturn(true); $encoder->expects($this->any())->method('encodePassword')->with('pa$$word', null)->willReturn('new-encoded-password'); $this->encoderFactory = $this->createMock(EncoderFactoryInterface::class); - $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->user)->willReturn($encoder); + $this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->callback(function ($user) { return $this->user->isEqualTo($user); }))->willReturn($encoder); $this->listener = new PasswordMigratingListener($this->encoderFactory); } @@ -96,12 +96,12 @@ public function testUpgradeWithUpgrader() public function testUpgradeWithoutUpgrader() { - $userLoader = $this->createMock(MigratingUserProvider::class); - $userLoader->expects($this->any())->method('loadUserByUsername')->willReturn($this->user); + $userLoader = $this->getMockBuilder(MigratingUserProvider::class)->setMethods(['upgradePassword'])->getMock(); + $userLoader->createUser($this->user); $userLoader->expects($this->once()) ->method('upgradePassword') - ->with($this->user, 'new-encoded-password') + ->with($this->callback(function ($user) { return $this->user->isEqualTo($user); }), 'new-encoded-password') ; $event = $this->createEvent(new SelfValidatingPassport(new UserBadge('test', [$userLoader, 'loadUserByUsername']), [new PasswordUpgradeBadge('pa$$word')])); @@ -119,7 +119,7 @@ private function createEvent(PassportInterface $passport) } } -abstract class MigratingUserProvider implements UserProviderInterface, PasswordUpgraderInterface +class MigratingUserProvider extends InMemoryUserProvider implements PasswordUpgraderInterface { public function upgradePassword(UserInterface $user, string $newEncodedPassword): void { diff --git a/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php b/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php index 95f99de8d0fde..2f7113e05e15d 100644 --- a/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/EventListener/UserProviderListenerTest.php @@ -12,9 +12,9 @@ namespace Symfony\Component\Security\Http\Tests\EventListener; use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; @@ -28,7 +28,7 @@ class UserProviderListenerTest extends TestCase protected function setUp(): void { - $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->userProvider = new InMemoryUserProvider(); $this->listener = new UserProviderListener($this->userProvider); } @@ -42,8 +42,8 @@ public function testSetUserProvider() $this->assertEquals([$this->userProvider, 'loadUserByUsername'], $badge->getUserLoader()); $user = new User('wouter', null); - $this->userProvider->expects($this->once())->method('loadUserByUsername')->with('wouter')->willReturn($user); - $this->assertSame($user, $passport->getUser()); + $this->userProvider->createUser($user); + $this->assertTrue($user->isEqualTo($passport->getUser())); } /** diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php index d8063f47960fb..559ef3e0ef1a2 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php @@ -22,11 +22,9 @@ use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserCheckerInterface; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Event\SwitchUserEvent; use Symfony\Component\Security\Http\Firewall\SwitchUserListener; use Symfony\Component\Security\Http\SecurityEvents; @@ -49,7 +47,7 @@ class SwitchUserListenerTest extends TestCase protected function setUp(): void { $this->tokenStorage = new TokenStorage(); - $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->userProvider = new InMemoryUserProvider(['kuba' => []]); $this->userChecker = $this->createMock(UserCheckerInterface::class); $this->accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class); $this->request = new Request(); @@ -112,13 +110,13 @@ public function testExitUserUpdatesToken() public function testExitUserDispatchesEventWithRefreshedUser() { - $originalUser = $this->createMock(UserInterface::class); - $refreshedUser = $this->createMock(UserInterface::class); - $this - ->userProvider + $originalUser = new User('username', null); + $refreshedUser = new User('username', null); + $userProvider = $this->createMock(InMemoryUserProvider::class); + $userProvider ->expects($this->any()) ->method('refreshUser') - ->with($originalUser) + ->with($this->identicalTo($originalUser)) ->willReturn($refreshedUser); $originalToken = new UsernamePasswordToken($originalUser, '', 'key'); $this->tokenStorage->setToken(new SwitchUserToken('username', '', 'key', ['ROLE_USER'], $originalToken)); @@ -136,15 +134,15 @@ public function testExitUserDispatchesEventWithRefreshedUser() ) ; - $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', $dispatcher); + $listener = new SwitchUserListener($this->tokenStorage, $userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', $dispatcher); $listener($this->event); } public function testExitUserDoesNotDispatchEventWithStringUser() { $originalUser = 'anon.'; - $this - ->userProvider + $userProvider = $this->createMock(InMemoryUserProvider::class); + $userProvider ->expects($this->never()) ->method('refreshUser'); $originalToken = new UsernamePasswordToken($originalUser, '', 'key'); @@ -157,7 +155,7 @@ public function testExitUserDoesNotDispatchEventWithStringUser() ->method('dispatch') ; - $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', $dispatcher); + $listener = new SwitchUserListener($this->tokenStorage, $userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', $dispatcher); $listener($this->event); } @@ -174,11 +172,6 @@ public function testSwitchUserIsDisallowed() ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH']) ->willReturn(false); - $this->userProvider->expects($this->exactly(2)) - ->method('loadUserByUsername') - ->withConsecutive(['kuba']) - ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); - $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); $listener($this->event); } @@ -189,16 +182,11 @@ public function testSwitchUserTurnsAuthenticationExceptionTo403() $token = new UsernamePasswordToken('username', '', 'key', ['ROLE_ALLOWED_TO_SWITCH']); $this->tokenStorage->setToken($token); - $this->request->query->set('_switch_user', 'kuba'); + $this->request->query->set('_switch_user', 'not-existing'); $this->accessDecisionManager->expects($this->never()) ->method('decide'); - $this->userProvider->expects($this->exactly(2)) - ->method('loadUserByUsername') - ->withConsecutive(['kuba'], ['username']) - ->will($this->onConsecutiveCalls($this->throwException(new UsernameNotFoundException()))); - $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); $listener($this->event); } @@ -206,21 +194,16 @@ public function testSwitchUserTurnsAuthenticationExceptionTo403() public function testSwitchUser() { $token = new UsernamePasswordToken('username', '', 'key', ['ROLE_FOO']); - $user = new User('username', 'password', []); $this->tokenStorage->setToken($token); $this->request->query->set('_switch_user', 'kuba'); $this->accessDecisionManager->expects($this->once()) - ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user) + ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $this->callback(function ($user) { return 'kuba' === $user->getUsername(); })) ->willReturn(true); - $this->userProvider->expects($this->exactly(2)) - ->method('loadUserByUsername') - ->withConsecutive(['kuba']) - ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); $this->userChecker->expects($this->once()) - ->method('checkPostAuth')->with($user); + ->method('checkPostAuth')->with($this->callback(function ($user) { return 'kuba' === $user->getUsername(); })); $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); $listener($this->event); @@ -238,20 +221,15 @@ public function testSwitchUserAlreadySwitched() $tokenStorage = new TokenStorage(); $tokenStorage->setToken($alreadySwitchedToken); - $targetUser = new User('kuba', 'password', ['ROLE_FOO', 'ROLE_BAR']); - $this->request->query->set('_switch_user', 'kuba'); + $targetsUser = $this->callback(function ($user) { return 'kuba' === $user->getUsername(); }); $this->accessDecisionManager->expects($this->once()) - ->method('decide')->with($originalToken, ['ROLE_ALLOWED_TO_SWITCH'], $targetUser) + ->method('decide')->with($originalToken, ['ROLE_ALLOWED_TO_SWITCH'], $targetsUser) ->willReturn(true); - $this->userProvider->expects($this->exactly(2)) - ->method('loadUserByUsername') - ->withConsecutive(['kuba']) - ->will($this->onConsecutiveCalls($targetUser, $this->throwException(new UsernameNotFoundException()))); $this->userChecker->expects($this->once()) - ->method('checkPostAuth')->with($targetUser); + ->method('checkPostAuth')->with($targetsUser); $listener = new SwitchUserListener($tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', null, false); $listener($this->event); @@ -265,22 +243,19 @@ public function testSwitchUserAlreadySwitched() public function testSwitchUserWorksWithFalsyUsernames() { - $token = new UsernamePasswordToken('username', '', 'key', ['ROLE_FOO']); - $user = new User('username', 'password', []); + $token = new UsernamePasswordToken('kuba', '', 'key', ['ROLE_FOO']); $this->tokenStorage->setToken($token); $this->request->query->set('_switch_user', '0'); + $this->userProvider->createUser($user = new User('0', null)); + $this->accessDecisionManager->expects($this->once()) ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH']) ->willReturn(true); - $this->userProvider->expects($this->exactly(2)) - ->method('loadUserByUsername') - ->withConsecutive(['0']) - ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); $this->userChecker->expects($this->once()) - ->method('checkPostAuth')->with($user); + ->method('checkPostAuth')->with($this->callback(function ($argUser) use ($user) { return $user->isEqualTo($argUser); })); $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); $listener($this->event); @@ -293,7 +268,6 @@ public function testSwitchUserWorksWithFalsyUsernames() public function testSwitchUserKeepsOtherQueryStringParameters() { $token = new UsernamePasswordToken('username', '', 'key', ['ROLE_FOO']); - $user = new User('username', 'password', []); $this->tokenStorage->setToken($token); $this->request->query->replace([ @@ -302,16 +276,13 @@ public function testSwitchUserKeepsOtherQueryStringParameters() 'section' => 2, ]); + $targetsUser = $this->callback(function ($user) { return 'kuba' === $user->getUsername(); }); $this->accessDecisionManager->expects($this->once()) - ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user) + ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $targetsUser) ->willReturn(true); - $this->userProvider->expects($this->exactly(2)) - ->method('loadUserByUsername') - ->withConsecutive(['kuba']) - ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); $this->userChecker->expects($this->once()) - ->method('checkPostAuth')->with($user); + ->method('checkPostAuth')->with($targetsUser); $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager); $listener($this->event); @@ -332,21 +303,16 @@ public function testSwitchUserWithReplacedToken() $this->request->query->set('_switch_user', 'kuba'); $this->accessDecisionManager->expects($this->any()) - ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user) + ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $this->callback(function ($user) { return 'kuba' === $user->getUsername(); })) ->willReturn(true); - $this->userProvider->expects($this->exactly(2)) - ->method('loadUserByUsername') - ->withConsecutive(['kuba']) - ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); - $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher ->expects($this->once()) ->method('dispatch') ->with( - $this->callback(function (SwitchUserEvent $event) use ($replacedToken, $user) { - if ($user !== $event->getTargetUser()) { + $this->callback(function (SwitchUserEvent $event) use ($replacedToken) { + if ('kuba' !== $event->getTargetUser()->getUsername()) { return false; } $event->setToken($replacedToken); @@ -374,21 +340,17 @@ public function testSwitchUserThrowsAuthenticationExceptionIfNoCurrentToken() public function testSwitchUserStateless() { $token = new UsernamePasswordToken('username', '', 'key', ['ROLE_FOO']); - $user = new User('username', 'password', []); $this->tokenStorage->setToken($token); $this->request->query->set('_switch_user', 'kuba'); + $targetsUser = $this->callback(function ($user) { return 'kuba' === $user->getUsername(); }); $this->accessDecisionManager->expects($this->once()) - ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $user) + ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $targetsUser) ->willReturn(true); - $this->userProvider->expects($this->exactly(2)) - ->method('loadUserByUsername') - ->withConsecutive(['kuba']) - ->will($this->onConsecutiveCalls($user, $this->throwException(new UsernameNotFoundException()))); $this->userChecker->expects($this->once()) - ->method('checkPostAuth')->with($user); + ->method('checkPostAuth')->with($targetsUser); $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', null, true); $listener($this->event); @@ -396,4 +358,34 @@ public function testSwitchUserStateless() $this->assertInstanceOf(UsernamePasswordToken::class, $this->tokenStorage->getToken()); $this->assertFalse($this->event->hasResponse()); } + + public function testSwitchUserRefreshesOriginalToken() + { + $originalUser = new User('username', null); + $refreshedOriginalUser = new User('username', null); + $userProvider = $this->createMock(InMemoryUserProvider::class); + $userProvider + ->expects($this->any()) + ->method('refreshUser') + ->with($this->identicalTo($originalUser)) + ->willReturn($refreshedOriginalUser); + $originalToken = new UsernamePasswordToken($originalUser, '', 'key'); + $this->tokenStorage->setToken(new SwitchUserToken('username', '', 'key', ['ROLE_USER'], $originalToken)); + $this->request->query->set('_switch_user', SwitchUserListener::EXIT_VALUE); + + $dispatcher = $this->createMock(EventDispatcherInterface::class); + $dispatcher + ->expects($this->once()) + ->method('dispatch') + ->with( + $this->callback(function (SwitchUserEvent $event) use ($refreshedOriginalUser) { + return $event->getToken()->getUser() === $refreshedOriginalUser; + }), + SecurityEvents::SWITCH_USER + ) + ; + + $listener = new SwitchUserListener($this->tokenStorage, $userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager, null, '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', $dispatcher); + $listener($this->event); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php index 05c6340b6aaa5..42c8094ba10b7 100644 --- a/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/LoginLink/LoginLinkHandlerTest.php @@ -29,7 +29,7 @@ class LoginLinkHandlerTest extends TestCase { /** @var MockObject|UrlGeneratorInterface */ private $router; - /** @var MockObject|UserProviderInterface */ + /** @var TestLoginLinkHandlerUserProvider */ private $userProvider; /** @var PropertyAccessorInterface */ private $propertyAccessor; @@ -39,7 +39,7 @@ class LoginLinkHandlerTest extends TestCase protected function setUp(): void { $this->router = $this->createMock(UrlGeneratorInterface::class); - $this->userProvider = $this->createMock(UserProviderInterface::class); + $this->userProvider = new TestLoginLinkHandlerUserProvider(); $this->propertyAccessor = PropertyAccess::createPropertyAccessor(); $this->expiredLinkStorage = $this->createMock(ExpiredLoginLinkStorage::class); } @@ -94,10 +94,7 @@ public function testConsumeLoginLink() $request = Request::create(sprintf('/login/verify?user=weaverryan&hash=%s&expires=%d', $signature, $expires)); $user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash'); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->with('weaverryan') - ->willReturn($user); + $this->userProvider->createUser($user); $this->expiredLinkStorage->expects($this->once()) ->method('incrementUsages') @@ -105,7 +102,7 @@ public function testConsumeLoginLink() $linker = $this->createLinker(['max_uses' => 3]); $actualUser = $linker->consumeLoginLink($request); - $this->assertSame($user, $actualUser); + $this->assertEquals($user, $actualUser); } public function testConsumeLoginLinkWithExpired() @@ -116,10 +113,7 @@ public function testConsumeLoginLinkWithExpired() $request = Request::create(sprintf('/login/verify?user=weaverryan&hash=%s&expires=%d', $signature, $expires)); $user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash'); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->with('weaverryan') - ->willReturn($user); + $this->userProvider->createUser($user); $linker = $this->createLinker(['max_uses' => 3]); $linker->consumeLoginLink($request); @@ -130,11 +124,6 @@ public function testConsumeLoginLinkWithUserNotFound() $this->expectException(InvalidLoginLinkException::class); $request = Request::create('/login/verify?user=weaverryan&hash=thehash&expires=10000'); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->with('weaverryan') - ->willThrowException(new UsernameNotFoundException()); - $linker = $this->createLinker(); $linker->consumeLoginLink($request); } @@ -145,10 +134,7 @@ public function testConsumeLoginLinkWithDifferentSignature() $request = Request::create(sprintf('/login/verify?user=weaverryan&hash=fake_hash&expires=%d', time() + 500)); $user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash'); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->with('weaverryan') - ->willReturn($user); + $this->userProvider->createUser($user); $linker = $this->createLinker(); $linker->consumeLoginLink($request); @@ -162,10 +148,7 @@ public function testConsumeLoginLinkExceedsMaxUsage() $request = Request::create(sprintf('/login/verify?user=weaverryan&hash=%s&expires=%d', $signature, $expires)); $user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash'); - $this->userProvider->expects($this->once()) - ->method('loadUserByUsername') - ->with('weaverryan') - ->willReturn($user); + $this->userProvider->createUser($user); $this->expiredLinkStorage->expects($this->once()) ->method('countUsages') @@ -198,6 +181,35 @@ private function createLinker(array $options = [], array $extraProperties = ['em } } +class TestLoginLinkHandlerUserProvider implements UserProviderInterface +{ + private $users = []; + + public function createUser(TestLoginLinkHandlerUser $user): void + { + $this->users[$user->getUsername()] = $user; + } + + public function loadUserByUsername(string $username): TestLoginLinkHandlerUser + { + if (!isset($this->users[$username])) { + throw new UsernameNotFoundException(); + } + + return clone $this->users[$username]; + } + + public function refreshUser(UserInterface $user) + { + return $this->users[$username]; + } + + public function supportsClass(string $class) + { + return TestLoginLinkHandlerUser::class === $class; + } +} + class TestLoginLinkHandlerUser implements UserInterface { public $username; diff --git a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentTokenBasedRememberMeServicesTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentTokenBasedRememberMeServicesTest.php index f85422fe60eb5..0efda325d3552 100644 --- a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentTokenBasedRememberMeServicesTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentTokenBasedRememberMeServicesTest.php @@ -22,9 +22,8 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\CookieTheftException; use Symfony\Component\Security\Core\Exception\TokenNotFoundException; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\RememberMe\PersistentTokenBasedRememberMeServices; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -94,12 +93,6 @@ public function testAutoLoginReturnsNullOnNonExistentUser() ; $service->setTokenProvider($tokenProvider); - $userProvider - ->expects($this->once()) - ->method('loadUserByUsername') - ->willThrowException(new UsernameNotFoundException('user not found')) - ; - $this->assertNull($service->autoLogin($request)); $this->assertTrue($request->attributes->has(RememberMeServicesInterface::COOKIE_ATTR_NAME)); } @@ -161,20 +154,10 @@ public function testAutoLoginDoesNotAcceptAnExpiredCookie() */ public function testAutoLogin(bool $hashTokenValue) { - $user = $this->createMock(UserInterface::class); - $user - ->expects($this->once()) - ->method('getRoles') - ->willReturn(['ROLE_FOO']) - ; + $user = new User('foouser', null, ['ROLE_FOO']); $userProvider = $this->getProvider(); - $userProvider - ->expects($this->once()) - ->method('loadUserByUsername') - ->with($this->equalTo('foouser')) - ->willReturn($user) - ; + $userProvider->createUser($user); $service = $this->getService($userProvider, ['name' => 'foo', 'path' => null, 'domain' => null, 'secure' => false, 'httponly' => false, 'always_remember_me' => true, 'lifetime' => 3600]); $request = new Request(); @@ -186,14 +169,14 @@ public function testAutoLogin(bool $hashTokenValue) ->expects($this->once()) ->method('loadTokenBySeries') ->with($this->equalTo('fooseries')) - ->willReturn(new PersistentToken('fooclass', 'foouser', 'fooseries', $tokenValue, new \DateTime())) + ->willReturn(new PersistentToken(User::class, 'foouser', 'fooseries', $tokenValue, new \DateTime())) ; $service->setTokenProvider($tokenProvider); $returnedToken = $service->autoLogin($request); $this->assertInstanceOf(RememberMeToken::class, $returnedToken); - $this->assertSame($user, $returnedToken->getUser()); + $this->assertTrue($user->isEqualTo($returnedToken->getUser())); $this->assertEquals('foosecret', $returnedToken->getSecret()); $this->assertTrue($request->attributes->has(RememberMeServicesInterface::COOKIE_ATTR_NAME)); } @@ -283,12 +266,7 @@ public function testLoginSuccessSetsCookieWhenLoggedInWithNonRememberMeTokenInte $request = new Request(); $response = new Response(); - $account = $this->createMock(UserInterface::class); - $account - ->expects($this->once()) - ->method('getUsername') - ->willReturn('foo') - ; + $account = new User('foo', null); $token = $this->createMock(TokenInterface::class); $token ->expects($this->any()) @@ -339,14 +317,7 @@ protected function getService($userProvider = null, $options = [], $logger = nul protected function getProvider() { - $provider = $this->createMock(UserProviderInterface::class); - $provider - ->expects($this->any()) - ->method('supportsClass') - ->willReturn(true) - ; - - return $provider; + return new InMemoryUserProvider(); } protected function generateHash(string $tokenValue): string diff --git a/src/Symfony/Component/Security/Http/Tests/RememberMe/TokenBasedRememberMeServicesTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/TokenBasedRememberMeServicesTest.php index 55cfb678f3014..cc8152047359a 100644 --- a/src/Symfony/Component/Security/Http/Tests/RememberMe/TokenBasedRememberMeServicesTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/TokenBasedRememberMeServicesTest.php @@ -18,9 +18,8 @@ use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; -use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; use Symfony\Component\Security\Http\RememberMe\TokenBasedRememberMeServices; @@ -51,12 +50,6 @@ public function testAutoLoginThrowsExceptionOnNonExistentUser() $request = new Request(); $request->cookies->set('foo', $this->getCookie('fooclass', 'foouser', time() + 3600, 'foopass')); - $userProvider - ->expects($this->once()) - ->method('loadUserByUsername') - ->willThrowException(new UsernameNotFoundException('user not found')) - ; - $this->assertNull($service->autoLogin($request)); $this->assertTrue($request->attributes->get(RememberMeServicesInterface::COOKIE_ATTR_NAME)->isCleared()); } @@ -68,19 +61,8 @@ public function testAutoLoginDoesNotAcceptCookieWithInvalidHash() $request = new Request(); $request->cookies->set('foo', base64_encode('class:'.base64_encode('foouser').':123456789:fooHash')); - $user = $this->createMock(UserInterface::class); - $user - ->expects($this->once()) - ->method('getPassword') - ->willReturn('foopass') - ; - - $userProvider - ->expects($this->once()) - ->method('loadUserByUsername') - ->with($this->equalTo('foouser')) - ->willReturn($user) - ; + $user = new User('foouser', 'foopass'); + $userProvider->createUser($user); $this->assertNull($service->autoLogin($request)); $this->assertTrue($request->attributes->get(RememberMeServicesInterface::COOKIE_ATTR_NAME)->isCleared()); @@ -93,19 +75,8 @@ public function testAutoLoginDoesNotAcceptAnExpiredCookie() $request = new Request(); $request->cookies->set('foo', $this->getCookie('fooclass', 'foouser', time() - 1, 'foopass')); - $user = $this->createMock(UserInterface::class); - $user - ->expects($this->once()) - ->method('getPassword') - ->willReturn('foopass') - ; - - $userProvider - ->expects($this->once()) - ->method('loadUserByUsername') - ->with($this->equalTo('foouser')) - ->willReturn($user) - ; + $user = new User('foouser', 'foopass'); + $userProvider->createUser($user); $this->assertNull($service->autoLogin($request)); $this->assertTrue($request->attributes->get(RememberMeServicesInterface::COOKIE_ATTR_NAME)->isCleared()); @@ -118,34 +89,18 @@ public function testAutoLoginDoesNotAcceptAnExpiredCookie() */ public function testAutoLogin($username) { - $user = $this->createMock(UserInterface::class); - $user - ->expects($this->once()) - ->method('getRoles') - ->willReturn(['ROLE_FOO']) - ; - $user - ->expects($this->once()) - ->method('getPassword') - ->willReturn('foopass') - ; - $userProvider = $this->getProvider(); - $userProvider - ->expects($this->once()) - ->method('loadUserByUsername') - ->with($this->equalTo($username)) - ->willReturn($user) - ; + $user = new User($username, 'foopass', ['ROLE_FOO']); + $userProvider->createUser($user); $service = $this->getService($userProvider, ['name' => 'foo', 'always_remember_me' => true, 'lifetime' => 3600]); $request = new Request(); - $request->cookies->set('foo', $this->getCookie('fooclass', $username, time() + 3600, 'foopass')); + $request->cookies->set('foo', $this->getCookie(User::class, $username, time() + 3600, 'foopass')); $returnedToken = $service->autoLogin($request); $this->assertInstanceOf(RememberMeToken::class, $returnedToken); - $this->assertSame($user, $returnedToken->getUser()); + $this->assertTrue($user->isEqualTo($returnedToken->getUser())); $this->assertEquals('foosecret', $returnedToken->getSecret()); } @@ -214,18 +169,8 @@ public function testLoginSuccess() $request = new Request(); $response = new Response(); + $user = new User('foouser', 'foopass'); $token = $this->createMock(TokenInterface::class); - $user = $this->createMock(UserInterface::class); - $user - ->expects($this->once()) - ->method('getPassword') - ->willReturn('foopass') - ; - $user - ->expects($this->once()) - ->method('getUsername') - ->willReturn('foouser') - ; $token ->expects($this->atLeastOnce()) ->method('getUser') @@ -279,13 +224,6 @@ protected function getService($userProvider = null, $options = [], $logger = nul protected function getProvider() { - $provider = $this->createMock(UserProviderInterface::class); - $provider - ->expects($this->any()) - ->method('supportsClass') - ->willReturn(true) - ; - - return $provider; + return new InMemoryUserProvider(); } } diff --git a/src/Symfony/Component/String/Inflector/EnglishInflector.php b/src/Symfony/Component/String/Inflector/EnglishInflector.php index addfac1344e8d..c2714e1c7b456 100644 --- a/src/Symfony/Component/String/Inflector/EnglishInflector.php +++ b/src/Symfony/Component/String/Inflector/EnglishInflector.php @@ -226,6 +226,9 @@ final class EnglishInflector implements InflectorInterface // bacteria (bacterium), criteria (criterion), phenomena (phenomenon) ['noi', 3, true, true, 'ions'], + // coupon (coupons) + ['nop', 3, true, true, 'pons'], + // seasons (season), treasons (treason), poisons (poison), lessons (lesson) ['nos', 3, true, true, 'sons'], diff --git a/src/Symfony/Component/String/Tests/EnglishInflectorTest.php b/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php similarity index 98% rename from src/Symfony/Component/String/Tests/EnglishInflectorTest.php rename to src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php index 04935568cde69..e1ce3284aea81 100644 --- a/src/Symfony/Component/String/Tests/EnglishInflectorTest.php +++ b/src/Symfony/Component/String/Tests/Inflector/EnglishInflectorTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\String\Tests; +namespace Symfony\Component\String\Tests\Inflector; use PHPUnit\Framework\TestCase; use Symfony\Component\String\Inflector\EnglishInflector; @@ -58,6 +58,7 @@ public function singularizeProvider() ['crises', ['cris', 'crise', 'crisis']], ['criteria', ['criterion', 'criterium']], ['cups', 'cup'], + ['coupons', 'coupon'], ['data', 'data'], ['days', 'day'], ['discos', 'disco'], @@ -201,6 +202,7 @@ public function pluralizeProvider() ['crisis', 'crises'], ['criteria', 'criterion'], ['cup', 'cups'], + ['coupon', 'coupons'], ['data', 'data'], ['day', 'days'], ['disco', 'discos'], diff --git a/src/Symfony/Component/String/Tests/FrenchInflectorTest.php b/src/Symfony/Component/String/Tests/Inflector/FrenchInflectorTest.php similarity index 98% rename from src/Symfony/Component/String/Tests/FrenchInflectorTest.php rename to src/Symfony/Component/String/Tests/Inflector/FrenchInflectorTest.php index bb975e2d3ac43..ff4deb4eac9aa 100644 --- a/src/Symfony/Component/String/Tests/FrenchInflectorTest.php +++ b/src/Symfony/Component/String/Tests/Inflector/FrenchInflectorTest.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\String\Tests; +namespace Symfony\Component\String\Tests\Inflector; use PHPUnit\Framework\TestCase; use Symfony\Component\String\Inflector\FrenchInflector; diff --git a/src/Symfony/Component/Translation/MessageCatalogue.php b/src/Symfony/Component/Translation/MessageCatalogue.php index d50ae03e97bbc..1dc3cc54a811e 100644 --- a/src/Symfony/Component/Translation/MessageCatalogue.php +++ b/src/Symfony/Component/Translation/MessageCatalogue.php @@ -163,7 +163,7 @@ public function add(array $messages, string $domain = 'messages') } $intlDomain = $domain; $suffixLength = \strlen(self::INTL_DOMAIN_SUFFIX); - if (\strlen($domain) > $suffixLength && false !== strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) { + if (\strlen($domain) < $suffixLength || false === strpos($domain, self::INTL_DOMAIN_SUFFIX, -$suffixLength)) { $intlDomain .= self::INTL_DOMAIN_SUFFIX; } foreach ($messages as $id => $message) { diff --git a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationPassTest.php b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php similarity index 95% rename from src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationPassTest.php rename to src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php index aede4bb472c11..c8247626b6507 100644 --- a/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslationPassTest.php +++ b/src/Symfony/Component/Translation/Tests/DependencyInjection/TranslatorPassTest.php @@ -18,7 +18,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; -class TranslationPassTest extends TestCase +class TranslatorPassTest extends TestCase { public function testValidCollector() { @@ -35,7 +35,7 @@ public function testValidCollector() $container->setDefinition('translation.reader', $reader); $container->setDefinition('translation.xliff_loader', $loader); - $pass = new TranslatorPass('translator.default', 'translation.reader'); + $pass = new TranslatorPass(); $pass->process($container); $expectedReader = (new Definition()) @@ -72,7 +72,7 @@ public function testValidCommandsViewPathsArgument() ; $container->setParameter('twig.default_path', 'templates'); - $pass = new TranslatorPass('translator.default'); + $pass = new TranslatorPass(); $pass->process($container); $expectedViewPaths = ['other/templates', 'tpl']; @@ -113,7 +113,7 @@ public function testCommandsViewPathsArgumentsAreIgnoredWithOldServiceDefinition ; $container->setParameter('twig.default_path', 'templates'); - $pass = new TranslatorPass('translator.default'); + $pass = new TranslatorPass(); $pass->process($container); $this->assertSame('templates', $debugCommand->getArgument(4)); diff --git a/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php b/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php index 788a72587e531..fb118e93afbb8 100644 --- a/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php +++ b/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php @@ -58,7 +58,7 @@ public function testAll() $this->assertEquals($messages, $catalogue->all()); } - public function testAllIntICU() + public function testAllIntlIcu() { $messages = [ 'domain1+intl-icu' => ['foo' => 'bar'], @@ -118,6 +118,16 @@ public function testAdd() $this->assertEquals('bar', $catalogue->get('foo', 'domain88')); } + public function testAddIntlIcu() + { + $catalogue = new MessageCatalogue('en', ['domain1+intl-icu' => ['foo' => 'foo']]); + $catalogue->add(['foo1' => 'foo1'], 'domain1'); + $catalogue->add(['foo' => 'bar'], 'domain1'); + + $this->assertSame('bar', $catalogue->get('foo', 'domain1')); + $this->assertSame('foo1', $catalogue->get('foo1', 'domain1')); + } + public function testReplace() { $catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain1+intl-icu' => ['bar' => 'bar']]); diff --git a/src/Symfony/Component/Uid/Uuid.php b/src/Symfony/Component/Uid/Uuid.php index 0a1327eedd445..4ed48b5735b9d 100644 --- a/src/Symfony/Component/Uid/Uuid.php +++ b/src/Symfony/Component/Uid/Uuid.php @@ -121,7 +121,7 @@ public function toRfc4122(): string return $this->uid; } - public function compare(parent $other): int + public function compare(AbstractUid $other): int { if (false !== $cmp = uuid_compare($this->uid, $other->uid)) { return $cmp; diff --git a/src/Symfony/Component/Validator/ConstraintViolation.php b/src/Symfony/Component/Validator/ConstraintViolation.php index 2004745fe238d..e9aa1bd9f4400 100644 --- a/src/Symfony/Component/Validator/ConstraintViolation.php +++ b/src/Symfony/Component/Validator/ConstraintViolation.php @@ -100,7 +100,7 @@ public function __toString() */ public function getMessageTemplate() { - return $this->messageTemplate; + return (string) $this->messageTemplate; } /** diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.nn.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.nn.xlf index db804d3b68eed..8963ba2d8c2c4 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.nn.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.nn.xlf @@ -36,7 +36,7 @@ This field was not expected. - Dette feltet var ikke forventa. + Dette feltet var ikkje forventa. This field is missing. @@ -248,7 +248,7 @@ This value should be equal to {{ compared_value }}. - Verdien bør vera like med {{ compared_value }}. + Verdien bør vera eins med {{ compared_value }}. This value should be greater than {{ compared_value }}. @@ -256,11 +256,11 @@ This value should be greater than or equal to {{ compared_value }}. - Verdien bør vera større enn eller så med {{ compared_value }}. + Verdien bør vera større enn eller eins med {{ compared_value }}. This value should be identical to {{ compared_value_type }} {{ compared_value }}. - Verdien bør vera identisk med {{ compared_value_type }} {{ compared_value }}. + Verdien bør vera eins med {{ compared_value_type }} {{ compared_value }}. This value should be less than {{ compared_value }}. @@ -268,47 +268,47 @@ This value should be less than or equal to {{ compared_value }}. - Verdi bør vera mindre enn eller så med {{ compared_value }}. + Verdi bør vera mindre enn eller eins med {{ compared_value }}. This value should not be equal to {{ compared_value }}. - Verdi bør ikkje vera så med {{ compared_value }}. + Verdi bør ikkje vera eins med {{ compared_value }}. This value should not be identical to {{ compared_value_type }} {{ compared_value }}. - Dette verdi bør ikkje vera identisk med {{ compared_value_type }} {{ compared_value }}. + Denne verdien bør ikkje vera eins med {{ compared_value_type }} {{ compared_value }}. The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}. - Bildetilhøvet er for stort ({{ ratio }}). Det tillatne maksimale tilhøvet er {{ max_ratio }}. + Sideforholdet til biletet er for stort ({{ ratio }}). Sideforholdet kan ikkje vere større enn {{ max_ratio }}. The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}. - Bildetilhøvet er for lite ({{ ratio }}). Forventa minimikvot er {{ min_ratio }}. + Sideforholdet til biletet er for lite ({{ ratio }}). Sideforholdet kan ikkje vere mindre enn {{ min_ratio }}. The image is square ({{ width }}x{{ height }}px). Square images are not allowed. - Bildet er firkanta ({{ width }}x{{ height }}px). Fyrkantiga bilde er ikkje tillatne. + Biletet er kvadratisk ({{ width }}x{{ height }}px). Kvadratiske bilete er ikkje tillatne. The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed. - Bildet er liggande orientert ({{ width }}x{{ height }}px). Landskapsorienterade bilde er ikkje tillatne. + Biletet er landskapsorientert ({{ width }}x{{ height }}px). Landskapsorienterte bilete er ikkje tillatne. The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed. - Bildet er porträttorienterad ({{ width }}x{{ height }}px). Porträttorienterade bilde er ikkje tillatne. + Biletet er portrettorientert ({{ width }}x{{ height }}px). Portrettorienterte bilete er ikkje tillatne. An empty file is not allowed. - Ein tom fil er ikkje tillaten. + Ei tom fil er ikkje tillate. The host could not be resolved. - Verdiar kunne ikkje løysast. + Verten kunne ikkje finnast. This value does not match the expected {{ charset }} charset. - Verdi stemmer ikkje med forventa {{ charset }} charset. + Verdien stemmer ikkje med forventa {{ charset }} charset. This is not a valid Business Identifier Code (BIC). @@ -324,7 +324,7 @@ This value should be a multiple of {{ compared_value }}. - Verdi bør vera eit multipel av {{ compared_value }}. + Verdien bør vera eit multipel av {{ compared_value }}. This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}. @@ -332,7 +332,7 @@ This value should be valid JSON. - Verdi bør vera gyldig JSON. + Verdien bør vera gyldig JSON. This collection should contain only unique elements. @@ -340,31 +340,31 @@ This value should be positive. - Verdi bør vera positivt. + Verdien bør vera positiv. This value should be either positive or zero. - Verdi bør vera enten positivt eller noll. + Verdien bør vera anten positiv eller null. This value should be negative. - Verdi bør vera negativt. + Verdien bør vera negativ. This value should be either negative or zero. - Verdi bør vera negativt eller noll. + Verdien bør vera negativ eller null. This value is not a valid timezone. - Verdi er ikkje ei gyldig tidssone. + Verdien er ikkje ei gyldig tidssone. This password has been leaked in a data breach, it must not be used. Please use another password. - Det her passordet har lekt ut ved eit datainnbrot, det får ikkje nyttast. Nytt eit anna passord. + Dette passordet har lekt ut ved eit datainnbrot, det får ikkje nyttast. Gje opp eit anna passord. This value should be between {{ min }} and {{ max }}. - Dette verdi bør ligga mellom {{ min }} og {{ max }}. + Denne verdien bør liggje mellom {{ min }} og {{ max }}. This value is not a valid hostname. @@ -372,15 +372,19 @@ The number of elements in this collection should be a multiple of {{ compared_value }}. - Talet på element i denne samlinga bør vera eit multipel av {{ compared_value }}. + Talet på element i denne samlinga bør vera eit multippel av {{ compared_value }}. This value should satisfy at least one of the following constraints: - Verdien burde oppfylla minst ein av følgjande begränsningar: + Verdien burde oppfylla minst ein av følgjande avgrensingar: Each element of this collection should satisfy its own set of constraints. - Kvart element i denne samlinga bør oppfylla sine eigne begränsningar. + Kvart element i denne samlinga bør oppfylla sine eigne avgrensingar. + + + This value is not a valid International Securities Identification Number (ISIN). + Verdien er ikkje eit gyldig International Securities Identification Number (ISIN). diff --git a/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php b/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php index a7734635aacff..dbac96a8aff4d 100644 --- a/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php +++ b/src/Symfony/Component/Validator/Tests/ConstraintViolationTest.php @@ -171,4 +171,19 @@ public function testRetrievedPropertyPathIsAStringEvenIfNotSet() ))->getPropertyPath() ); } + + public function testRetrievedMessageTemplateIsAStringEvenIfNotSet() + { + self::assertSame( + '', + (new ConstraintViolation( + 'irrelevant', + null, + [], + 'irrelevant', + 'irrelevant', + null + ))->getMessageTemplate() + ); + } } diff --git a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php index b1f1384ffbd16..7920a07c8d9f4 100644 --- a/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php +++ b/src/Symfony/Component/Validator/Validator/RecursiveContextualValidator.php @@ -649,8 +649,10 @@ private function validateGenericNode($value, ?object $object, ?string $cacheKey, return; } - // If the value is a scalar, pass it anyway, because we want - // a NoSuchMetadataException to be thrown in that case + if (!\is_object($value)) { + throw new NoSuchMetadataException(sprintf('Cannot create metadata for non-objects. Got: "%s".', \gettype($value))); + } + $this->validateObject( $value, $propertyPath, diff --git a/src/Symfony/Component/VarDumper/Caster/DateCaster.php b/src/Symfony/Component/VarDumper/Caster/DateCaster.php index e3708b7fb3276..1f61c327af309 100644 --- a/src/Symfony/Component/VarDumper/Caster/DateCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/DateCaster.php @@ -91,7 +91,7 @@ public static function castPeriod(\DatePeriod $p, array $a, Stub $stub, bool $is $dates = []; foreach (clone $p as $i => $d) { if (self::PERIOD_LIMIT === $i) { - $now = new \DateTimeImmutable(); + $now = new \DateTimeImmutable('now', new \DateTimeZone('UTC')); $dates[] = sprintf('%s more', ($end = $p->getEndDate()) ? ceil(($end->format('U.u') - $d->format('U.u')) / ((int) $now->add($p->getDateInterval())->format('U.u') - (int) $now->format('U.u'))) : $p->recurrences - $i diff --git a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php index b003cba95c0b2..c850e0b1d3a6c 100644 --- a/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php +++ b/src/Symfony/Component/VarDumper/Caster/ReflectionCaster.php @@ -96,11 +96,20 @@ public static function castType(\ReflectionType $c, array $a, Stub $stub, bool $ { $prefix = Caster::PREFIX_VIRTUAL; - $a += [ - $prefix.'name' => $c instanceof \ReflectionNamedType ? $c->getName() : (string) $c, - $prefix.'allowsNull' => $c->allowsNull(), - $prefix.'isBuiltin' => $c->isBuiltin(), - ]; + if ($c instanceof \ReflectionNamedType || \PHP_VERSION_ID < 80000) { + $a += [ + $prefix.'name' => $c instanceof \ReflectionNamedType ? $c->getName() : (string) $c, + $prefix.'allowsNull' => $c->allowsNull(), + $prefix.'isBuiltin' => $c->isBuiltin(), + ]; + } elseif ($c instanceof \ReflectionUnionType) { + $a[$prefix.'allowsNull'] = $c->allowsNull(); + self::addMap($a, $c, [ + 'types' => 'getTypes', + ]); + } else { + $a[$prefix.'allowsNull'] = $c->allowsNull(); + } return $a; } diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php index 796a31e4e041d..c1cc61504c902 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/DateCasterTest.php @@ -341,7 +341,7 @@ public function testDumpPeriod($start, $interval, $end, $options, $expected) */ public function testCastPeriod($start, $interval, $end, $options, $xPeriod, $xDates) { - $p = new \DatePeriod(new \DateTime($start), new \DateInterval($interval), \is_int($end) ? $end : new \DateTime($end), $options); + $p = new \DatePeriod(new \DateTime($start, new \DateTimeZone('UTC')), new \DateInterval($interval), \is_int($end) ? $end : new \DateTime($end, new \DateTimeZone('UTC')), $options); $stub = new Stub(); $cast = DateCaster::castPeriod($p, [], $stub, false, 0); diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index a5d059ce231f1..2987044f882a7 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -14,9 +14,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Test\VarDumperTestTrait; +use Symfony\Component\VarDumper\Tests\Fixtures\ExtendsReflectionTypeFixture; use Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo; use Symfony\Component\VarDumper\Tests\Fixtures\LotsOfAttributes; use Symfony\Component\VarDumper\Tests\Fixtures\NotLoadableClass; +use Symfony\Component\VarDumper\Tests\Fixtures\ReflectionNamedTypeFixture; +use Symfony\Component\VarDumper\Tests\Fixtures\ReflectionUnionTypeFixture; /** * @author Nicolas Grekas @@ -91,7 +94,7 @@ public function testClosureCaster() $b: & 123 } file: "%sReflectionCasterTest.php" - line: "84 to 84" + line: "87 to 87" } EOTXT , $var @@ -227,6 +230,104 @@ public function testReflectionParameterNullableUnion() ); } + /** + * @requires PHP 7.4 + */ + public function testReflectionPropertyScalar() + { + $var = new \ReflectionProperty(ReflectionNamedTypeFixture::class, 'a'); + $this->assertDumpMatchesFormat( + <<<'EOTXT' +ReflectionProperty { + +name: "a" + +class: "Symfony\Component\VarDumper\Tests\Fixtures\ReflectionNamedTypeFixture" + modifiers: "public" +} +EOTXT + , $var + ); + } + + /** + * @requires PHP 7.4 + */ + public function testReflectionNamedType() + { + $var = (new \ReflectionProperty(ReflectionNamedTypeFixture::class, 'a'))->getType(); + $this->assertDumpMatchesFormat( + <<<'EOTXT' +ReflectionNamedType { + name: "int" + allowsNull: false + isBuiltin: true +} +EOTXT + , $var + ); + } + + /** + * @requires PHP 8 + */ + public function testReflectionUnionType() + { + $var = (new \ReflectionProperty(ReflectionUnionTypeFixture::class, 'a'))->getType(); + $this->assertDumpMatchesFormat( + <<<'EOTXT' +ReflectionUnionType { + allowsNull: false + types: array:2 [ + 0 => ReflectionNamedType { + name: "string" + allowsNull: false + isBuiltin: true + } + 1 => ReflectionNamedType { + name: "int" + allowsNull: false + isBuiltin: true + } + ] +} +EOTXT + , $var + ); + } + + /** + * @requires PHP 8 + */ + public function testExtendsReflectionType() + { + $var = new ExtendsReflectionTypeFixture(); + $this->assertDumpMatchesFormat( + <<<'EOTXT' +Symfony\Component\VarDumper\Tests\Fixtures\ExtendsReflectionTypeFixture { + allowsNull: false +} +EOTXT + , $var + ); + } + + /** + * @requires PHP < 8 + */ + public function testLegacyExtendsReflectionType() + { + $var = new ExtendsReflectionTypeFixture(); + $this->assertDumpMatchesFormat( + <<<'EOTXT' +Symfony\Component\VarDumper\Tests\Fixtures\ExtendsReflectionTypeFixture { + name: "fake" + allowsNull: false + isBuiltin: false +} +EOTXT + , $var + ); + } + public function testReturnType() { $f = eval('return function ():int {};'); diff --git a/src/Symfony/Component/VarDumper/Tests/Fixtures/ExtendsReflectionTypeFixture.php b/src/Symfony/Component/VarDumper/Tests/Fixtures/ExtendsReflectionTypeFixture.php new file mode 100644 index 0000000000000..1ee3d8c3844a7 --- /dev/null +++ b/src/Symfony/Component/VarDumper/Tests/Fixtures/ExtendsReflectionTypeFixture.php @@ -0,0 +1,21 @@ + '?Psr\Log\LoggerInterface'] * - * @return array The required service types, optionally keyed by service names + * @return string[] The required service types, optionally keyed by service names */ public static function getSubscribedServices(); } diff --git a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php index 82fb5ab361559..81b2bae8a4499 100644 --- a/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php +++ b/src/Symfony/Contracts/Service/ServiceSubscriberTrait.php @@ -24,6 +24,9 @@ trait ServiceSubscriberTrait /** @var ContainerInterface */ protected $container; + /** + * {@inheritdoc} + */ public static function getSubscribedServices(): array { static $services; diff --git a/src/Symfony/Contracts/Translation/README.md b/src/Symfony/Contracts/Translation/README.md index 6c693ce0b35ff..42e5c51754ed6 100644 --- a/src/Symfony/Contracts/Translation/README.md +++ b/src/Symfony/Contracts/Translation/README.md @@ -6,4 +6,4 @@ A set of abstractions extracted out of the Symfony components. Can be used to build on semantics that the Symfony components proved useful - and that already have battle tested implementations. -See https://github.com/symfony/contracts/blob/master/README.md for more information. +See https://github.com/symfony/contracts/blob/main/README.md for more information.