diff --git a/.travis.yml b/.travis.yml index 4638be740549b..3f34da8c38d19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -141,6 +141,12 @@ before_install: (cd php-$MIN_PHP && ./configure --enable-sigchild --enable-pcntl && make -j2) fi + - | + # Install vulcain + wget https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz -O - | tar xz + sudo mv vulcain /usr/local/bin + docker pull php:7.3-alpine + - | # php.ini configuration for PHP in $TRAVIS_PHP_VERSION $php_extra; do @@ -260,6 +266,11 @@ install: run_tests () { set -e export PHP=$1 + + if [[ !$deps && $PHP = 7.2 ]]; then + tfold src/Symfony/Component/HttpClient.h2push "$COMPOSER_UP symfony/contracts && docker run -it --rm -v $(pwd):/app -v $(phpenv which composer):/usr/local/bin/composer -v /usr/local/bin/vulcain:/usr/local/bin/vulcain -w /app php:7.3-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push" + fi + if [[ $PHP != 7.4* && $PHP != $TRAVIS_PHP_VERSION && $TRAVIS_PULL_REQUEST != false ]]; then echo -e "\\n\\e[33;1mIntermediate PHP version $PHP is skipped for pull requests.\\e[0m" return @@ -307,7 +318,9 @@ install: fi echo "$COMPONENTS" | parallel --gnu "tfold {} $PHPUNIT_X {}" + tfold src/Symfony/Component/Console.tty $PHPUNIT --group tty + if [[ $PHP = ${MIN_PHP%.*} ]]; then export PHP=$MIN_PHP tfold src/Symfony/Component/Process.sigchild SYMFONY_DEPRECATIONS_HELPER=weak php-$MIN_PHP/sapi/cli/php ./phpunit --colors=always src/Symfony/Component/Process/ diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md index 0f975d9e83dd5..64e77d4a4d718 100644 --- a/CHANGELOG-4.4.md +++ b/CHANGELOG-4.4.md @@ -7,6 +7,13 @@ in 4.4 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/v4.4.0...v4.4.1 +* 4.4.0 (2019-11-21) + + * bug #34464 [Form] group constraints when calling the validator (nicolas-grekas) + * bug #34451 [DependencyInjection] Fix dumping multiple deprecated aliases (shyim) + * bug #34448 [Form] allow button names to start with uppercase letter (xabbuh) + * bug #34428 [Security] Fix best encoder not wired using migrate_from (chalasr) + * 4.4.0-RC1 (2019-11-17) * bug #34419 [Cache] Disable igbinary on PHP >= 7.4 (nicolas-grekas) diff --git a/CHANGELOG-5.0.md b/CHANGELOG-5.0.md index 87ac1bdc29b58..920d9960fb944 100644 --- a/CHANGELOG-5.0.md +++ b/CHANGELOG-5.0.md @@ -7,6 +7,60 @@ in 5.0 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.0.0...v5.0.1 +* 5.0.1 (2019-12-01) + + * bug #34732 [DependencyInjection][Xml] Fix the attribute 'tag' is not allowed in 'bind' tag (tienvx) + * bug #34729 [DI] auto-register singly implemented interfaces by default (nicolas-grekas) + * bug #34728 [DI] fix overriding existing services with aliases for singly-implemented interfaces (nicolas-grekas) + * bug #34649 more robust initialization from request (dbu) + * bug #34715 [TwigBundle] remove service when base class is missing (xabbuh) + * bug #34600 [DoctrineBridge] do not depend on the QueryBuilder from the ORM (xabbuh) + * bug #34627 [Security/Http] call auth listeners/guards eagerly when they "support" the request (nicolas-grekas) + * bug #34671 [Security] Fix clearing remember-me cookie after deauthentication (chalasr) + * bug #34711 Fix the translation commands when a template contains a syntax error (fabpot) + * bug #34032 [Mime] Fixing multidimensional array structure with FormDataPart (jvahldick) + * bug #34697 [MonologBridge] Fix compatibility of ServerLogHandler with Monolog 2 (jderusse) + * bug #34560 [Config][ReflectionClassResource] Handle parameters with undefined constant as their default values (fancyweb) + * bug #34695 [Config] don't break on virtual stack frames in ClassExistenceResource (nicolas-grekas) + * bug #34716 [DependencyInjection] fix dumping number-like string parameters (xabbuh) + * bug #34558 [Console] Fix autocomplete multibyte input support (fancyweb) + * bug #34130 [Console] Fix commands description with numeric namespaces (fancyweb) + * bug #34562 [DI] Skip unknown method calls for factories in check types pass (fancyweb) + * bug #34677 [EventDispatcher] Better error reporting when arguments to dispatch() are swapped (rimas-kudelis) + * bug #33573 [TwigBridge] Add row_attr to all form themes (fancyweb) + * bug #34019 [Serializer] CsvEncoder::NO_HEADERS_KEY ignored when used in constructor (Dario Savella) + * bug #34083 [Form] Keep preferred_choices order for choice groups (vilius-g) + * bug #34091 [Debug] work around failing chdir() on Darwin (mary2501) + * bug #34305 [PhpUnitBridge] Read configuration CLI directive (ro0NL) + * bug #34490 [Serializer] Fix MetadataAwareNameConverter usage with string group (antograssiot) + * bug #34632 [Console] Fix trying to access array offset on value of type int (Tavafi) + * bug #34669 [HttpClient] turn exception into log when the request has no content-type (nicolas-grekas) + * bug #34662 [HttpKernel] Support typehint to deprecated FlattenException in controller (andrew-demb) + * bug #34619 Restores preview mode support for Html and Serializer error renderers (yceruto) + * bug #34636 [VarDumper] notice on potential undefined index (sylvainmetayer) + * bug #34668 [Cache] Make sure we get the correct number of values from redis::mget() (thePanz) + * bug #34621 [Routing] Continue supporting single colon in object route loaders (fancyweb) + * bug #34554 [HttpClient] Fix early cleanup of pushed HTTP/2 responses (lyrixx) + * bug #34607 [HttpKernel] Ability to define multiple kernel.reset tags (rmikalkenas) + * bug #34599 [Mailer][Mailchimp Bridge] Throwing undefined index _id when setting message id (monteiro) + * bug #34569 [Workflow] Apply the same logic of precedence between the apply() and the buildTransitionBlockerList() method (lyrixx) + * bug #34580 [HttpKernel] Don't cache "not-fresh" state (nicolas-grekas) + * bug #34577 [FrameworkBundle][Cache] Don't deep-merge cache pools configuration (alxndrbauer) + * bug #34515 [DependencyInjection] definitions are valid objects (xabbuh) + * bug #34536 [SecurityBundle] Don't require a user provider for the anonymous listener (chalasr) + * bug #34533 [Monolog Bridge] Fixed accessing static property as non static. (Sander-Toonen) + * bug #34502 [FrameworkBundle][ContainerLint] Keep "removing" compiler passes (fancyweb) + * bug #34552 [Dotenv] don't fail when referenced env var does not exist (xabbuh) + * bug #34546 [Serializer] Add DateTimeZoneNormalizer into Dependency Injection (jewome62) + * bug #34547 [Messenger] Error when specified default bus is not among the configured (vudaltsov) + * bug #34513 [Validator] remove return type declaration from __sleep() (xabbuh) + * bug #34551 [Security] SwitchUser is broken when the User Provider always returns a valid user (tucksaun) + * bug #34570 [FrameworkBundle][Notifier] Fixing notifier email definition without mailer (chr-hertel) + * bug #34385 Avoid empty "If-Modified-Since" header in validation request (mpdude) + * bug #34458 [Validator] ConstraintValidatorTestCase: add missing return value to mocked validate method calls (ogizanagi) + * bug #34516 [HttpKernel] drop return type declaration (xabbuh) + * bug #34474 [Messenger] Ignore stamps in in-memory transport (tienvx) + * 5.0.0 (2019-11-21) * bug #34464 [Form] group constraints when calling the validator (nicolas-grekas) diff --git a/README.md b/README.md index 5796b1acd7ceb..da9e6156c00d7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

-[Symfony][1] is a **PHP framework** for web applications and a set of reusable +[Symfony][1] is a **PHP framework** for web and console applications and a set of reusable **PHP components**. Symfony is used by thousands of web applications (including BlaBlaCar.com and Spotify.com) and most of the [popular PHP projects][2] (including Drupal and Magento). diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 1d1863e4f84a2..f3377f43da2eb 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -104,6 +104,9 @@ HttpFoundation * `ApacheRequest` is deprecated, use `Request` class instead. * Passing a third argument to `HeaderBag::get()` is deprecated since Symfony 4.4, use method `all()` instead + * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column, + make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to + update your database. * `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column, make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database to speed up garbage collection of expired sessions. @@ -168,6 +171,8 @@ Mailer ------ * [BC BREAK] Changed the DSN to use for disabling delivery (using the `NullTransport`) from `smtp://null` to `null://null` (host doesn't matter). + * [BC BREAK] Renamed class `SmtpEnvelope` to `Envelope` and `DelayedSmtpEnvelope` to `DelayedEnvelope`. + * [BC BREAK] Added a required `string $transport` argument to `MessageEvent::__construct`. Messenger --------- @@ -217,6 +222,7 @@ Security * The `LdapUserProvider` class has been deprecated, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead. * Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` should add a new `needsRehash()` method * Deprecated returning a non-boolean value when implementing `Guard\AuthenticatorInterface::checkCredentials()`. Please explicitly return `false` to indicate invalid credentials. + * The `ListenerInterface` is deprecated, extend `AbstractListener` instead. * Deprecated passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()` (and indirectly the `is_granted()` Twig and ExpressionLanguage function) **Before** @@ -228,7 +234,7 @@ Security **After** ```php - if ($this->authorizationChecker->isGranted(new Expression("has_role('ROLE_USER') or has_role('ROLE_ADMIN')"))) {} + if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {} // or: if ($this->authorizationChecker->isGranted('ROLE_USER') @@ -269,7 +275,17 @@ TwigBridge TwigBundle ---------- - * Deprecated `twig.exception_controller` configuration option, set it to "null" and use `framework.error_controller` instead: + * Deprecated `twig.exception_controller` configuration option. + + If you were not using this option previously, set it to `null`: + + After: + ```yaml + twig: + exception_controller: null + ``` + + If you were using this option previously, set it to `null` and use `framework.error_controller` instead: Before: ```yaml diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 9dbd28e2a3f41..3e67bb5e4d808 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -271,6 +271,9 @@ HttpFoundation * `ApacheRequest` has been removed, use the `Request` class instead. * The third argument of the `HeaderBag::get()` method has been removed, use method `all()` instead. * Getting the container from a non-booted kernel is not possible anymore. + * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column, + make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to + update your database. HttpKernel ---------- @@ -410,7 +413,7 @@ Security **After** ```php - if ($this->authorizationChecker->isGranted(new Expression("has_role('ROLE_USER') or has_role('ROLE_ADMIN')"))) {} + if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {} // or: if ($this->authorizationChecker->isGranted('ROLE_USER') @@ -434,7 +437,7 @@ Security * `SimpleAuthenticatorInterface`, `SimpleFormAuthenticatorInterface`, `SimplePreAuthenticatorInterface`, `SimpleAuthenticationProvider`, `SimpleAuthenticationHandler`, `SimpleFormAuthenticationListener` and `SimplePreAuthenticationListener` have been removed. Use Guard instead. - * The `ListenerInterface` has been removed, turn your listeners into callables instead. + * The `ListenerInterface` has been removed, extend `AbstractListener` instead. * The `Firewall::handleRequest()` method has been removed, use `Firewall::callListeners()` instead. * `\Serializable` interface has been removed from `AbstractToken` and `AuthenticationException`, thus `serialize()` and `unserialize()` aren't available. @@ -473,6 +476,7 @@ Security * Classes implementing the `TokenInterface` must implement the two new methods `__serialize` and `__unserialize` * Implementations of `Guard\AuthenticatorInterface::checkCredentials()` must return a boolean value now. Please explicitly return `false` to indicate invalid credentials. + * Removed the `has_role()` function from security expressions, use `is_granted()` instead. SecurityBundle -------------- diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php index c42d8117100c3..f63aca5937e77 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php @@ -218,10 +218,7 @@ private function getManagerName(ContainerBuilder $container): string } } - throw new InvalidArgumentException(sprintf( - 'Could not find the manager name parameter in the container. Tried the following parameter names: "%s"', - implode('", "', $this->managerParameters) - )); + throw new InvalidArgumentException(sprintf('Could not find the manager name parameter in the container. Tried the following parameter names: "%s"', implode('", "', $this->managerParameters))); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index 87c1f8263f877..4e06569b4d6d3 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -87,7 +87,7 @@ public function loadValuesForChoices(array $choices, callable $value = null) $optimize = $this->idReader && (null === $value || \is_array($value) && $value[0] === $this->idReader); // Attention: This optimization does not check choices for existence - if ($optimize && !$this->choiceList && $this->idReader->isSingleId()) { + if ($optimize && !$this->choiceList) { $values = []; // Maintain order and indices of the given objects @@ -123,7 +123,7 @@ public function loadChoicesForValues(array $values, callable $value = null) // a single-field identifier $optimize = $this->idReader && (null === $value || \is_array($value) && $this->idReader === $value[0]); - if ($optimize && !$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) { + if ($optimize && !$this->choiceList && $this->objectLoader) { $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values); $objectsById = []; $objects = []; diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 8589c3bd042b0..eff1245d41fa2 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -50,7 +50,6 @@ abstract class DoctrineType extends AbstractType implements ResetInterface * * For backwards compatibility, objects are cast to strings by default. * - * * @internal This method is public to be usable as callback. It should not * be used in user code. */ @@ -83,13 +82,16 @@ public static function createChoiceName(object $choice, $key, string $value): st * For instance in ORM two query builders with an equal SQL string and * equal parameters are considered to be equal. * + * @param object $queryBuilder A query builder, type declaration is not present here as there + * is no common base class for the different implementations + * * @return array|null Array with important QueryBuilder parts or null if * they can't be determined * * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public function getQueryBuilderPartsForCachingHash(QueryBuilder $queryBuilder): ?array + public function getQueryBuilderPartsForCachingHash($queryBuilder): ?array { return null; } @@ -143,8 +145,7 @@ public function configureOptions(OptionsResolver $resolver) $options['em'], $options['class'], $options['id_reader'], - $entityLoader, - false + $entityLoader ); if (null !== $hash) { diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 2cdd8109138cc..6429ede4ca4ab 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -50,6 +50,10 @@ public function configureOptions(OptionsResolver $resolver) */ public function getLoader(ObjectManager $manager, QueryBuilder $queryBuilder, string $class) { + if (!$queryBuilder instanceof QueryBuilder) { + throw new \TypeError(sprintf('Expected an instance of %s, but got %s.', QueryBuilder::class, \is_object($queryBuilder) ? \get_class($queryBuilder) : \gettype($queryBuilder))); + } + return new ORMQueryBuilderLoader($queryBuilder); } @@ -65,11 +69,17 @@ public function getBlockPrefix() * We consider two query builders with an equal SQL string and * equal parameters to be equal. * + * @param QueryBuilder $queryBuilder + * * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public function getQueryBuilderPartsForCachingHash(QueryBuilder $queryBuilder): ?array + public function getQueryBuilderPartsForCachingHash($queryBuilder): ?array { + if (!$queryBuilder instanceof QueryBuilder) { + throw new \TypeError(sprintf('Expected an instance of %s, but got %s.', QueryBuilder::class, \is_object($queryBuilder) ? \get_class($queryBuilder) : \gettype($queryBuilder))); + } + return [ $queryBuilder->getQuery()->getSQL(), array_map([$this, 'parameterToArray'], $queryBuilder->getParameters()->toArray()), diff --git a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php index b1d9fcd51e242..e7049c4b614cc 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php @@ -41,7 +41,7 @@ public function onKernelResponse(ResponseEvent $event) } if (!preg_match(static::USER_AGENT_REGEX, $event->getRequest()->headers->get('User-Agent'))) { - $this->sendHeaders = false; + self::$sendHeaders = false; $this->headers = []; return; @@ -59,7 +59,7 @@ public function onKernelResponse(ResponseEvent $event) */ protected function sendHeader($header, $content): void { - if (!$this->sendHeaders) { + if (!self::$sendHeaders) { return; } diff --git a/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php b/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php index 389e64d5aca8f..4016d402b9634 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php @@ -12,14 +12,43 @@ namespace Symfony\Bridge\Monolog\Handler; use Monolog\Formatter\FormatterInterface; -use Monolog\Handler\AbstractHandler; +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Handler\FormattableHandlerTrait; use Monolog\Logger; use Symfony\Bridge\Monolog\Formatter\VarDumperFormatter; +if (trait_exists(FormattableHandlerTrait::class)) { + class ServerLogHandler extends AbstractProcessingHandler + { + use ServerLogHandlerTrait; + + /** + * {@inheritdoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new VarDumperFormatter(); + } + } +} else { + class ServerLogHandler extends AbstractProcessingHandler + { + use ServerLogHandlerTrait; + + /** + * {@inheritdoc} + */ + protected function getDefaultFormatter() + { + return new VarDumperFormatter(); + } + } +} + /** * @author Grégoire Pineau */ -class ServerLogHandler extends AbstractHandler +trait ServerLogHandlerTrait { private $host; private $context; @@ -56,6 +85,11 @@ public function handle(array $record): bool restore_error_handler(); } + return parent::handle($record); + } + + protected function write(array $record): void + { $recordFormatted = $this->formatRecord($record); set_error_handler(self::class.'::nullErrorHandler'); @@ -72,16 +106,12 @@ public function handle(array $record): bool } finally { restore_error_handler(); } - - return false === $this->bubble; } /** * {@inheritdoc} - * - * @return FormatterInterface */ - protected function getDefaultFormatter() + protected function getDefaultFormatter(): FormatterInterface { return new VarDumperFormatter(); } @@ -103,13 +133,7 @@ private function createSocket() private function formatRecord(array $record): string { - if ($this->processors) { - foreach ($this->processors as $processor) { - $record = $processor($record); - } - } - - $recordFormatted = $this->getFormatter()->format($record); + $recordFormatted = $record['formatted']; foreach (['log_uuid', 'uuid', 'uid'] as $key) { if (isset($record['extra'][$key])) { diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index 6ccfa74ce3e98..8ac4a79beb804 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -14,15 +14,21 @@ error_reporting(-1); -$getEnvVar = function ($name, $default = false) { +global $argv, $argc; +$argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : array(); +$argc = isset($_SERVER['argc']) ? $_SERVER['argc'] : 0; +$getEnvVar = function ($name, $default = false) use ($argv) { if (false !== $value = getenv($name)) { return $value; } static $phpunitConfig = null; if (null === $phpunitConfig) { + $opt = min(array_search('-c', $opts = array_reverse($argv), true) ?: INF, array_search('--configuration', $opts, true) ?: INF); $phpunitConfigFilename = null; - if (file_exists('phpunit.xml')) { + if (INF !== $opt && isset($opts[$opt - 1])) { + $phpunitConfigFilename = $opts[$opt - 1]; + } elseif (file_exists('phpunit.xml')) { $phpunitConfigFilename = 'phpunit.xml'; } elseif (file_exists('phpunit.xml.dist')) { $phpunitConfigFilename = 'phpunit.xml.dist'; @@ -185,10 +191,6 @@ class SymfonyBlacklistPhpunit {} chdir($oldPwd); } -global $argv, $argc; -$argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : array(); -$argc = isset($_SERVER['argc']) ? $_SERVER['argc'] : 0; - if ($PHPUNIT_VERSION < 8.0) { $argv = array_filter($argv, function ($v) use (&$argc) { if ('--do-not-cache-result' !== $v) return true; --$argc; return false; }); } elseif (filter_var(getenv('SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE'), FILTER_VALIDATE_BOOLEAN)) { diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig index b082d9236b927..4dc3e9c896fb7 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig @@ -27,7 +27,7 @@ col-sm-2 {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+ {{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}} @@ -38,7 +38,7 @@ col-sm-2 {%- endblock form_row %} {% block submit_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} @@ -47,7 +47,7 @@ col-sm-2 {%- endblock submit_row %} {% block reset_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} @@ -60,7 +60,7 @@ col-sm-10 {%- endblock form_group_class %} {% block checkbox_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig index 7d70044e6c8b8..f0cf0ad81854d 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig @@ -111,7 +111,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+ {{- form_label(form) }} {# -#} {{ form_widget(form, widget_attr) }} {# -#} {{- form_help(form) -}} @@ -120,7 +120,7 @@ {%- endblock form_row %} {% block button_row -%} -
+ {{- form_widget(form) -}}
{%- endblock button_row %} @@ -146,14 +146,14 @@ {%- endblock datetime_row %} {% block checkbox_row -%} -
+ {{- form_widget(form) -}} {{- form_errors(form) -}}
{%- endblock checkbox_row %} {% block radio_row -%} -
+ {{- form_widget(form) -}} {{- form_errors(form) -}}
diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig index e37de07d6b071..a75e364187743 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig @@ -28,7 +28,7 @@ col-sm-2 {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+ {{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}} @@ -43,7 +43,7 @@ col-sm-2 {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+
{{- form_label(form) -}}
@@ -55,7 +55,7 @@ col-sm-2 {%- endblock fieldset_form_row %} {% block submit_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} @@ -64,7 +64,7 @@ col-sm-2 {%- endblock submit_row %} {% block reset_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} @@ -77,7 +77,7 @@ col-sm-10 {%- endblock form_group_class %} {% block checkbox_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} 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 117ddedf6bc90..88f3d4aecc073 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 @@ -287,7 +287,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} - <{{ element|default('div') }} class="form-group"> + <{{ element|default('div') }}{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}> {{- form_label(form) -}} {{- form_widget(form, widget_attr) -}} {{- form_help(form) -}} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig index f74bd57002bba..1b0092859dbd9 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig @@ -187,7 +187,7 @@ {# Rows #} {% block button_row -%} -
+ {{- form_widget(form) -}}
{%- endblock button_row %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index ff8faad141cff..a97a38e5204e3 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -336,7 +336,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+ {{- form_label(form) -}} {{- form_errors(form) -}} {{- form_widget(form, widget_attr) -}} @@ -345,7 +345,7 @@ {%- endblock form_row -%} {%- block button_row -%} -
+ {{- form_widget(form) -}}
{%- endblock button_row -%} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig index 10eaf566d097d..255d6b42bf7bd 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig @@ -5,7 +5,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} - + {{- form_label(form) -}} @@ -18,7 +18,7 @@ {%- endblock form_row -%} {%- block button_row -%} - + {{- form_widget(form) -}} @@ -27,7 +27,8 @@ {%- endblock button_row -%} {%- block hidden_row -%} - + {%- set style = row_attr.style is defined ? (row_attr.style ~ (row_attr.style|trim|last != ';' ? '; ')) : '' -%} + {{- form_widget(form) -}} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig index d8bb8308a36aa..83c5e30d803bc 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig @@ -277,7 +277,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+
{{- form_label(form) -}} {{- form_widget(form, widget_attr) -}} @@ -308,7 +308,7 @@ {%- endblock datetime_row %} {% block checkbox_row -%} -
+
{{ form_widget(form) }} {{ form_errors(form) }} @@ -317,7 +317,7 @@ {%- endblock checkbox_row %} {% block radio_row -%} -
+
{{ form_widget(form) }} {{ form_errors(form) }} diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index 3f497202b4ed5..b8fa860f37980 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -17,7 +17,6 @@ use Symfony\Component\Translation\MessageCatalogue; use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Environment; -use Twig\Error\Error; use Twig\Loader\ArrayLoader; class TwigExtractorTest extends TestCase @@ -74,31 +73,23 @@ public function getExtractData() /** * @dataProvider resourcesWithSyntaxErrorsProvider */ - public function testExtractSyntaxError($resources) + public function testExtractSyntaxError($resources, array $messages) { - $this->expectException('Twig\Error\Error'); $twig = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock()); $twig->addExtension(new TranslationExtension($this->getMockBuilder(TranslatorInterface::class)->getMock())); $extractor = new TwigExtractor($twig); - - try { - $extractor->extract($resources, new MessageCatalogue('en')); - } catch (Error $e) { - $this->assertSame(\dirname(__DIR__).strtr('/Fixtures/extractor/syntax_error.twig', '/', \DIRECTORY_SEPARATOR), $e->getFile()); - $this->assertSame(1, $e->getLine()); - $this->assertSame('Unclosed "block".', $e->getMessage()); - - throw $e; - } + $catalogue = new MessageCatalogue('en'); + $extractor->extract($resources, $catalogue); + $this->assertSame($messages, $catalogue->all()); } public function resourcesWithSyntaxErrorsProvider(): array { return [ - [__DIR__.'/../Fixtures'], - [__DIR__.'/../Fixtures/extractor/syntax_error.twig'], - [new \SplFileInfo(__DIR__.'/../Fixtures/extractor/syntax_error.twig')], + [__DIR__.'/../Fixtures', ['messages' => ['Hi!' => 'Hi!']]], + [__DIR__.'/../Fixtures/extractor/syntax_error.twig', []], + [new \SplFileInfo(__DIR__.'/../Fixtures/extractor/syntax_error.twig'), []], ]; } diff --git a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php index b75ba559b7d16..e9e13395c75d9 100644 --- a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php +++ b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php @@ -12,7 +12,6 @@ namespace Symfony\Bridge\Twig\Translation; use Symfony\Component\Finder\Finder; -use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\Translation\Extractor\AbstractFileExtractor; use Symfony\Component\Translation\Extractor\ExtractorInterface; use Symfony\Component\Translation\MessageCatalogue; @@ -58,13 +57,7 @@ public function extract($resource, MessageCatalogue $catalogue) try { $this->extractTemplate(file_get_contents($file->getPathname()), $catalogue); } catch (Error $e) { - if ($file instanceof \SplFileInfo) { - $path = $file->getRealPath() ?: $file->getPathname(); - $name = $file instanceof SplFileInfo ? $file->getRelativePathname() : $path; - $e->setSourceContext(new Source('', $name, $path)); - } - - throw $e; + // ignore errors, these should be fixed by using the linter } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index bf4c4e9a7d3e6..8188769bdd29e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -39,7 +39,7 @@ CHANGELOG * Added new `error_controller` configuration to handle system exceptions * Added sort option for `translation:update` command. * [BC Break] The `framework.messenger.routing.senders` config key is not deeply merged anymore. - * Added `secrets:*` commands and `%env(secret:...)%` processor to deal with secrets seamlessly. + * Added `secrets:*` commands to deal with secrets seamlessly. * Made `framework.session.handler_id` accept a DSN * Marked the `RouterDataCollector` class as `@final`. * [BC Break] The `framework.messenger.buses..middleware` config key is not deeply merged anymore. diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index c5cba6c9a5286..cba4ac58408d6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -70,7 +70,6 @@ private function getContainerBuilder(): ContainerBuilder if (!$kernel->isDebug() || !(new ConfigCache($kernel->getContainer()->getParameter('debug.container.dump'), true))->isFresh()) { $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel)); $container = $buildContainer(); - $container->getCompilerPassConfig()->setRemovingPasses([]); } else { (new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php index 1b0fbdf4cec44..cc322847d0262 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php @@ -64,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); - $io->comment('Use "%env(secret:)%" to reference a secret in a config file.'); + $io->comment('Use "%env()%" to reference a secret in a config file.'); if (!$reveal = $input->getOption('reveal')) { $io->comment(sprintf('To reveal the secrets run php %s %s --reveal', $_SERVER['PHP_SELF'], $this->getName())); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php index 555d616712504..5cca8d7011fac 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php @@ -56,7 +56,7 @@ protected function configure() %command.full_name% To reference secrets in services.yaml or any other config -files, use "%env(secret:)%". +files, use "%env()%". By default, the secret value should be entered interactively. Alternatively, provide a file where to read the secret from: diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 952079cd17303..9d6e9e9645d81 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -19,6 +19,7 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\Form\Form; use Symfony\Component\HttpClient\HttpClient; @@ -877,6 +878,7 @@ private function addCacheSection(ArrayNodeDefinition $rootNode) ->end() ->children() ->arrayNode('adapters') + ->performNoDeepMerging() ->info('One or more adapters to chain for creating the pool, defaults to "cache.app".') ->beforeNormalization() ->always()->then(function ($values) { @@ -1035,6 +1037,10 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode) ->ifTrue(function ($v) { return isset($v['buses']) && \count($v['buses']) > 1 && null === $v['default_bus']; }) ->thenInvalid('You must specify the "default_bus" if you define more than one bus.') ->end() + ->validate() + ->ifTrue(static function ($v): bool { return isset($v['buses']) && null !== $v['default_bus'] && !isset($v['buses'][$v['default_bus']]); }) + ->then(static function (array $v): void { throw new InvalidConfigurationException(sprintf('The specified default bus "%s" is not configured. Available buses are "%s".', $v['default_bus'], implode('", "', array_keys($v['buses'])))); }) + ->end() ->children() ->arrayNode('routing') ->normalizeKeys(false) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 9f8f78bf36570..33b8f963eeb96 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -302,7 +302,7 @@ public function load(array $configs, ContainerBuilder $container) } if ($this->messengerConfigEnabled = $this->isConfigEnabled($container, $config['messenger'])) { - $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $config['serializer'], $config['validation']); + $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $config['validation']); } else { $container->removeDefinition('console.command.messenger_consume_messages'); $container->removeDefinition('console.command.messenger_debug'); @@ -1510,7 +1510,7 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont $storeDefinitions = []; foreach ($resourceStores as $storeDsn) { $storeDsn = $container->resolveEnvPlaceholders($storeDsn, null, $usedEnvs); - $storeDefinition = new Definition(PersistingStoreInterface::class); + $storeDefinition = new Definition(interface_exists(StoreInterface::class) ? StoreInterface::class : PersistingStoreInterface::class); $storeDefinition->setFactory([StoreFactory::class, 'createStore']); $storeDefinition->setArguments([$storeDsn]); @@ -1558,7 +1558,7 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont } } - private function registerMessengerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $serializerConfig, array $validationConfig) + private function registerMessengerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $validationConfig) { if (!interface_exists(MessageBusInterface::class)) { throw new LogicException('Messenger support cannot be enabled as the Messenger component is not installed. Try running "composer require symfony/messenger".'); @@ -1941,7 +1941,9 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $ // as we have a bus, the channels don't need the transports $container->getDefinition('notifier.channel.chat')->setArgument(0, null); - $container->getDefinition('notifier.channel.email')->setArgument(0, null); + if ($container->hasDefinition('notifier.channel.email')) { + $container->getDefinition('notifier.channel.email')->setArgument(0, null); + } $container->getDefinition('notifier.channel.sms')->setArgument(0, null); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml index af80a51d3f67b..4d2423feeeede 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml @@ -6,27 +6,26 @@ - %kernel.debug% + + + + + %kernel.debug% + + %kernel.charset% %kernel.project_dir% - - - - - - - + - + - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 4698c505930a3..0dbc388ddffcb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -12,8 +12,6 @@ - - @@ -40,6 +38,11 @@ + + + + + @@ -147,5 +150,26 @@ + + + + + + + + + + + + + + + + + + %kernel.debug% + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 9c3ab3cd8aa56..8d8e3a3a9e10f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -314,6 +314,27 @@ public function testBusMiddlewareDontMerge() ); } + public function testItErrorsWhenDefaultBusDoesNotExist() + { + $processor = new Processor(); + $configuration = new Configuration(true); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The specified default bus "foo" is not configured. Available buses are "bar", "baz".'); + + $processor->processConfiguration($configuration, [ + [ + 'messenger' => [ + 'default_bus' => 'foo', + 'buses' => [ + 'bar' => null, + 'baz' => null, + ], + ], + ], + ]); + } + protected static function getBundleDefaultConfig() { return [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php index df55260d8c607..13c368fd585e4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php @@ -37,6 +37,6 @@ public function build(ContainerBuilder $container) $extension->setCustomConfig(new CustomConfig()); $container->addCompilerPass(new AnnotationReaderPass(), PassConfig::TYPE_AFTER_REMOVING); - $container->addCompilerPass(new CheckTypeDeclarationsPass(true, ['http_client', '.debug.http_client']), PassConfig::TYPE_AFTER_REMOVING, -100); + $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 6a90421361d78..1f373a89265ce 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -20,7 +20,8 @@ "ext-xml": "*", "symfony/cache": "^4.4|^5.0", "symfony/config": "^5.0", - "symfony/dependency-injection": "^5.0", + "symfony/dependency-injection": "^5.0.1", + "symfony/error-handler": "^4.4.1|^5.0.1", "symfony/http-foundation": "^4.4|^5.0", "symfony/http-kernel": "^5.0", "symfony/polyfill-mbstring": "~1.0", diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index 3cc260d3cf50c..6853bcf712df0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -83,7 +83,11 @@ public function create(ContainerBuilder $container, string $id, array $config, ? throw new \RuntimeException('Each "security.remember_me_aware" tag must have a provider attribute.'); } - $userProviders[] = new Reference($attribute['provider']); + // context listeners don't need a provider + if ('none' !== $attribute['provider']) { + $userProviders[] = new Reference($attribute['provider']); + } + $container ->getDefinition($serviceId) ->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)]) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 9b54e1f9141ef..f4c13752a7763 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -315,10 +315,11 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $listeners[] = new Reference('security.channel_listener'); $contextKey = null; + $contextListenerId = null; // Context serializer listener if (false === $firewall['stateless']) { $contextKey = $firewall['context'] ?? $id; - $listeners[] = new Reference($this->createContextListener($container, $contextKey)); + $listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey)); $sessionStrategyId = 'security.authentication.session_strategy'; } else { $this->statelessFirewallKeys[] = $id; @@ -391,7 +392,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null; // Authentication listeners - list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint); + list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint); @@ -404,9 +405,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ } // Access listener - if ($firewall['stateless'] || empty($firewall['anonymous']['lazy'])) { - $listeners[] = new Reference('security.access_listener'); - } + $listeners[] = new Reference('security.access_listener'); // Exception listener $exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless'])); @@ -444,7 +443,7 @@ private function createContextListener(ContainerBuilder $container, string $cont return $this->contextListeners[$contextKey] = $listenerId; } - private function createAuthenticationListeners(ContainerBuilder $container, string $id, array $firewall, array &$authenticationProviders, ?string $defaultProvider, array $providerIds, ?string $defaultEntryPoint) + private function createAuthenticationListeners(ContainerBuilder $container, string $id, array $firewall, array &$authenticationProviders, ?string $defaultProvider, array $providerIds, ?string $defaultEntryPoint, string $contextListenerId = null) { $listeners = []; $hasListeners = false; @@ -459,9 +458,13 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$key]['provider'])); } $userProvider = $providerIds[$normalizedName]; - } elseif ('remember_me' === $key) { - // RememberMeFactory will use the firewall secret when created + } elseif ('remember_me' === $key || 'anonymous' === $key) { + // RememberMeFactory will use the firewall secret when created, AnonymousAuthenticationListener does not load users. $userProvider = null; + + if ('remember_me' === $key && $contextListenerId) { + $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); + } } elseif ($defaultProvider) { $userProvider = $defaultProvider; } elseif (empty($providerIds)) { diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 0408a48077f50..1f0e64b803484 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -151,9 +151,7 @@ - - diff --git a/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php index 78ee1f275e28c..30229b392048d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php @@ -13,11 +13,8 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; -use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; -use Symfony\Component\Security\Core\Exception\LazyResponseException; -use Symfony\Component\Security\Http\AccessMapInterface; use Symfony\Component\Security\Http\Event\LazyResponseEvent; -use Symfony\Component\Security\Http\Firewall\AccessListener; +use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\Firewall\ExceptionListener; use Symfony\Component\Security\Http\Firewall\LogoutListener; @@ -28,17 +25,13 @@ */ class LazyFirewallContext extends FirewallContext { - private $accessListener; private $tokenStorage; - private $map; - public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, AccessListener $accessListener, TokenStorage $tokenStorage, AccessMapInterface $map) + public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, TokenStorage $tokenStorage) { parent::__construct($listeners, $exceptionListener, $logoutListener, $config); - $this->accessListener = $accessListener; $this->tokenStorage = $tokenStorage; - $this->map = $map; } public function getListeners(): iterable @@ -48,21 +41,37 @@ public function getListeners(): iterable public function __invoke(RequestEvent $event) { - $this->tokenStorage->setInitializer(function () use ($event) { - $event = new LazyResponseEvent($event); - foreach (parent::getListeners() as $listener) { - $listener($event); + $listeners = []; + $request = $event->getRequest(); + $lazy = $request->isMethodCacheable(); + + foreach (parent::getListeners() as $listener) { + if (!$lazy || !$listener instanceof AbstractListener) { + $listeners[] = $listener; + $lazy = $lazy && $listener instanceof AbstractListener; + } elseif (false !== $supports = $listener->supports($request)) { + $listeners[] = [$listener, 'authenticate']; + $lazy = null === $supports; } - }); + } - try { - [$attributes] = $this->map->getPatterns($event->getRequest()); + if (!$lazy) { + foreach ($listeners as $listener) { + $listener($event); - if ($attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes) { - ($this->accessListener)($event); + if ($event->hasResponse()) { + return; + } } - } catch (LazyResponseException $e) { - $event->setResponse($e->getResponse()); + + return; } + + $this->tokenStorage->setInitializer(function () use ($event, $listeners) { + $event = new LazyResponseEvent($event); + foreach ($listeners as $listener) { + $listener($event); + } + }); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 8979ea8ded6a9..4b6c05bfb36a5 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -210,7 +210,7 @@ public function testMissingProviderForListener() $container->compile(); } - public function testPerListenerProviderWithRememberMe() + public function testPerListenerProviderWithRememberMeAndAnonymous() { $container = $this->getRawContainer(); $container->loadFromExtension('security', [ @@ -223,6 +223,7 @@ public function testPerListenerProviderWithRememberMe() 'default' => [ 'form_login' => ['provider' => 'second'], 'remember_me' => ['secret' => 'baz'], + 'anonymous' => true, ], ], ]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php new file mode 100644 index 0000000000000..fef2732759fa1 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; + +class AppCustomAuthenticator extends AbstractGuardAuthenticator +{ + public function supports(Request $request) + { + return true; + } + + public function getCredentials(Request $request) + { + throw new AuthenticationException('This should be hit'); + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + } + + public function checkCredentials($credentials, UserInterface $user) + { + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + return new Response('', 418); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + } + + public function start(Request $request, AuthenticationException $authException = null) + { + return new Response($authException->getMessage(), Response::HTTP_UNAUTHORIZED); + } + + public function supportsRememberMe() + { + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php new file mode 100644 index 0000000000000..3a1046b0c4a17 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +use Symfony\Component\HttpFoundation\Response; +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; + +class ClearRememberMeTest extends AbstractWebTestCase +{ + public function testUserChangeClearsCookie() + { + $client = $this->createClient(['test_case' => 'ClearRememberMe', 'root_config' => 'config.yml']); + + $client->request('POST', '/login', [ + '_username' => 'johannes', + '_password' => 'test', + ]); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $cookieJar = $client->getCookieJar(); + $this->assertNotNull($cookieJar->get('REMEMBERME')); + + $client->request('GET', '/foo'); + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertNull($cookieJar->get('REMEMBERME')); + } +} + +class RememberMeFooController +{ + public function __invoke(UserInterface $user) + { + return new Response($user->getUsername()); + } +} + +class RememberMeUserProvider implements UserProviderInterface +{ + private $inner; + + public function __construct(InMemoryUserProvider $inner) + { + $this->inner = $inner; + } + + public function loadUserByUsername($username) + { + return $this->inner->loadUserByUsername($username); + } + + public function refreshUser(UserInterface $user) + { + $user = $this->inner->refreshUser($user); + + $alterUser = \Closure::bind(function (User $user) { $user->password = 'foo'; }, null, User::class); + $alterUser($user); + + return $user; + } + + public function supportsClass($class) + { + return $this->inner->supportsClass($class); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/GuardedTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/GuardedTest.php new file mode 100644 index 0000000000000..bb0969c36a2fd --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/GuardedTest.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +class GuardedTest extends AbstractWebTestCase +{ + public function testGuarded() + { + $client = $this->createClient(['test_case' => 'Guarded', 'root_config' => 'config.yml']); + + $client->request('GET', '/'); + + $this->assertSame(418, $client->getResponse()->getStatusCode()); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/bundles.php new file mode 100644 index 0000000000000..9a26fb163a77d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/bundles.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\SecurityBundle\SecurityBundle; + +return [ + new FrameworkBundle(), + new SecurityBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/config.yml new file mode 100644 index 0000000000000..a0ed6f8e1e151 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/config.yml @@ -0,0 +1,31 @@ +imports: + - { resource: ./../config/framework.yml } + +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + + providers: + in_memory: + memory: + users: + johannes: { password: test, roles: [ROLE_USER] } + + firewalls: + default: + form_login: + check_path: login + remember_me: true + remember_me: + always_remember_me: true + secret: key + anonymous: ~ + + access_control: + - { path: ^/foo, roles: ROLE_USER } + +services: + Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeUserProvider: + public: true + decorates: security.user.provider.concrete.in_memory + arguments: ['@Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeUserProvider.inner'] diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/routing.yml new file mode 100644 index 0000000000000..08975bdcb3832 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/routing.yml @@ -0,0 +1,7 @@ +login: + path: /login + +foo: + path: /foo + defaults: + _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeFooController diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/bundles.php new file mode 100644 index 0000000000000..d1e9eb7e0d36a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/bundles.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\SecurityBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml new file mode 100644 index 0000000000000..2d1f779a530ec --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml @@ -0,0 +1,22 @@ +framework: + secret: test + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" } + test: ~ + default_locale: en + profiler: false + session: + storage_id: session.storage.mock_file + +services: + logger: { class: Psr\Log\NullLogger } + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AppCustomAuthenticator: ~ + +security: + firewalls: + secure: + pattern: ^/ + anonymous: lazy + stateless: false + guard: + authenticators: + - Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AppCustomAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/routing.yml new file mode 100644 index 0000000000000..4d11154375219 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/routing.yml @@ -0,0 +1,5 @@ +main: + path: / + defaults: + _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction + path: /app diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 18c86174ba978..a87e4a1f00d25 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -24,7 +24,7 @@ "symfony/security-core": "^4.4|^5.0", "symfony/security-csrf": "^4.4|^5.0", "symfony/security-guard": "^4.4|^5.0", - "symfony/security-http": "^4.4|^5.0" + "symfony/security-http": "^4.4.1|^5.0.1" }, "require-dev": { "doctrine/doctrine-bundle": "^1.5|^2.0", diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index 6c955aae6fa50..062d843cd0a73 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\TwigBundle\DependencyInjection\Compiler; +use Symfony\Bridge\Twig\Extension\AssetExtension; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; diff --git a/src/Symfony/Bundle/WebServerBundle/.gitattributes b/src/Symfony/Bundle/WebServerBundle/.gitattributes deleted file mode 100644 index ebb9287043dc4..0000000000000 --- a/src/Symfony/Bundle/WebServerBundle/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -/Tests export-ignore -/phpunit.xml.dist export-ignore -/.gitignore export-ignore diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 582d133958600..5a8460a0d2824 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -308,7 +308,13 @@ protected function doFetch(array $ids) } }); } else { - $values = array_combine($ids, $this->redis->mget($ids)); + $values = $this->redis->mget($ids); + + if (!\is_array($values) || \count($values) !== \count($ids)) { + return []; + } + + $values = array_combine($ids, $values); } foreach ($values as $id => $v) { diff --git a/src/Symfony/Component/Config/Definition/VariableNode.php b/src/Symfony/Component/Config/Definition/VariableNode.php index fd193d8e73418..e868ece13e543 100644 --- a/src/Symfony/Component/Config/Definition/VariableNode.php +++ b/src/Symfony/Component/Config/Definition/VariableNode.php @@ -84,7 +84,7 @@ protected function finalizeValue($value) // deny environment variables only when using custom validators // this avoids ever passing an empty value to final validation closures if (!$this->allowEmptyValue && $this->isHandlingPlaceholder() && $this->finalValidationClosures) { - $e = new InvalidConfigurationException(sprintf('The path "%s" cannot contain an environment variable when empty values are not allowed by definition and are validated.', $this->getPath(), json_encode($value))); + $e = new InvalidConfigurationException(sprintf('The path "%s" cannot contain an environment variable when empty values are not allowed by definition and are validated.', $this->getPath())); if ($hint = $this->getInfo()) { $e->addHint($hint); } diff --git a/src/Symfony/Component/Config/Resource/ClassExistenceResource.php b/src/Symfony/Component/Config/Resource/ClassExistenceResource.php index ed9d7627a36ee..5ad88cca6ba6c 100644 --- a/src/Symfony/Component/Config/Resource/ClassExistenceResource.php +++ b/src/Symfony/Component/Config/Resource/ClassExistenceResource.php @@ -72,7 +72,7 @@ public function isFresh(int $timestamp): bool spl_autoload_register(__CLASS__.'::throwOnRequiredClass'); } $autoloadedClass = self::$autoloadedClass; - self::$autoloadedClass = $this->resource; + self::$autoloadedClass = ltrim($this->resource, '\\'); try { $exists = class_exists($this->resource) || interface_exists($this->resource, false) || trait_exists($this->resource, false); @@ -153,7 +153,7 @@ public static function throwOnRequiredClass(string $class, \Exception $previous throw $e; } - $trace = $e->getTrace(); + $trace = debug_backtrace(); $autoloadFrame = [ 'function' => 'spl_autoload_call', 'args' => [$class], @@ -183,15 +183,17 @@ public static function throwOnRequiredClass(string $class, \Exception $previous } $props = [ - 'file' => $trace[$i]['file'], - 'line' => $trace[$i]['line'], + 'file' => isset($trace[$i]['file']) ? $trace[$i]['file'] : null, + 'line' => isset($trace[$i]['line']) ? $trace[$i]['line'] : null, 'trace' => \array_slice($trace, 1 + $i), ]; foreach ($props as $p => $v) { - $r = new \ReflectionProperty('Exception', $p); - $r->setAccessible(true); - $r->setValue($e, $v); + if (null !== $v) { + $r = new \ReflectionProperty('Exception', $p); + $r->setAccessible(true); + $r->setValue($e, $v); + } } } diff --git a/src/Symfony/Component/Config/Resource/ReflectionClassResource.php b/src/Symfony/Component/Config/Resource/ReflectionClassResource.php index a23f10435feda..bca92690db899 100644 --- a/src/Symfony/Component/Config/Resource/ReflectionClassResource.php +++ b/src/Symfony/Component/Config/Resource/ReflectionClassResource.php @@ -143,12 +143,56 @@ private function generateSignature(\ReflectionClass $class): iterable } foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) { - yield preg_replace('/^ @@.*/m', '', $m); - $defaults = []; + $parametersWithUndefinedConstants = []; foreach ($m->getParameters() as $p) { - $defaults[$p->name] = $p->isDefaultValueAvailable() ? $p->getDefaultValue() : null; + if (!$p->isDefaultValueAvailable()) { + $defaults[$p->name] = null; + + continue; + } + + if (!$p->isDefaultValueConstant() || \defined($p->getDefaultValueConstantName())) { + $defaults[$p->name] = $p->getDefaultValue(); + + continue; + } + + $defaults[$p->name] = $p->getDefaultValueConstantName(); + $parametersWithUndefinedConstants[$p->name] = true; } + + if (!$parametersWithUndefinedConstants) { + yield preg_replace('/^ @@.*/m', '', $m); + } else { + $stack = [ + $m->getDocComment(), + $m->getName(), + $m->isAbstract(), + $m->isFinal(), + $m->isStatic(), + $m->isPublic(), + $m->isPrivate(), + $m->isProtected(), + $m->returnsReference(), + $m->hasReturnType() ? $m->getReturnType()->getName() : '', + ]; + + foreach ($m->getParameters() as $p) { + if (!isset($parametersWithUndefinedConstants[$p->name])) { + $stack[] = (string) $p; + } else { + $stack[] = $p->isOptional(); + $stack[] = $p->hasType() ? $p->getType()->getName() : ''; + $stack[] = $p->isPassedByReference(); + $stack[] = $p->isVariadic(); + $stack[] = $p->getName(); + } + } + + yield implode(',', $stack); + } + yield print_r($defaults, true); } diff --git a/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php index 0296af3d121da..629cd0b3ff6d4 100644 --- a/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php +++ b/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php @@ -64,8 +64,12 @@ public function testIsFreshForDeletedResources() /** * @dataProvider provideHashedSignature */ - public function testHashedSignature($changeExpected, $changedLine, $changedCode) + public function testHashedSignature($changeExpected, $changedLine, $changedCode, $setContext = null) { + if ($setContext) { + $setContext(); + } + $code = <<<'EOPHP' /* 0*/ /* 1*/ class %s extends ErrorException @@ -83,7 +87,9 @@ public function testHashedSignature($changeExpected, $changedLine, $changedCode) /*13*/ protected function prot($a = []) {} /*14*/ /*15*/ private function priv() {} -/*16*/ } +/*16*/ +/*17*/ public function ccc($bar = A_CONSTANT_THAT_FOR_SURE_WILL_NEVER_BE_DEFINED_CCCCCC) {} +/*18*/ } EOPHP; static $expectedSignature, $generateSignature; @@ -98,7 +104,9 @@ public function testHashedSignature($changeExpected, $changedLine, $changedCode) } $code = explode("\n", $code); - $code[$changedLine] = $changedCode; + if (null !== $changedCode) { + $code[$changedLine] = $changedCode; + } eval(sprintf(implode("\n", $code), $class = 'Foo'.str_replace('.', '_', uniqid('', true)))); $signature = implode("\n", iterator_to_array($generateSignature(new \ReflectionClass($class)))); @@ -142,6 +150,10 @@ public function provideHashedSignature(): iterable yield [0, 7, 'protected int $prot;']; yield [0, 9, 'private string $priv;']; } + + yield [1, 17, 'public function ccc($bar = 187) {}']; + yield [1, 17, 'public function ccc($bar = ANOTHER_ONE_THAT_WILL_NEVER_BE_DEFINED_CCCCCCCCC) {}']; + yield [1, 17, null, static function () { \define('A_CONSTANT_THAT_FOR_SURE_WILL_NEVER_BE_DEFINED_CCCCCC', 'foo'); }]; } public function testEventSubscriber() diff --git a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php index dda38cb81213e..af8836391264c 100644 --- a/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php +++ b/src/Symfony/Component/Console/Descriptor/ApplicationDescription.php @@ -115,23 +115,29 @@ private function sortCommands(array $commands): array { $namespacedCommands = []; $globalCommands = []; + $sortedCommands = []; foreach ($commands as $name => $command) { $key = $this->application->extractNamespace($name, 1); - if (!$key) { - $globalCommands['_global'][$name] = $command; + if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) { + $globalCommands[$name] = $command; } else { $namespacedCommands[$key][$name] = $command; } } - ksort($namespacedCommands); - $namespacedCommands = array_merge($globalCommands, $namespacedCommands); - foreach ($namespacedCommands as &$commandsSet) { - ksort($commandsSet); + if ($globalCommands) { + ksort($globalCommands); + $sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands; } - // unset reference to keep scope clear - unset($commandsSet); - return $namespacedCommands; + if ($namespacedCommands) { + ksort($namespacedCommands); + foreach ($namespacedCommands as $key => $commandsSet) { + ksort($commandsSet); + $sortedCommands[$key] = $commandsSet; + } + } + + return $sortedCommands; } } diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 80906db4eda21..faf2648bd1bd1 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -230,7 +230,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu } elseif ("\177" === $c) { // Backspace Character if (0 === $numMatches && 0 !== $i) { --$i; - $fullChoice = substr($fullChoice, 0, -1); + $fullChoice = self::substr($fullChoice, 0, $i); // Move cursor backwards $output->write("\033[1D"); } @@ -244,7 +244,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu } // Pop the last character off the end of our string - $ret = substr($ret, 0, $i); + $ret = self::substr($ret, 0, $i); } elseif ("\033" === $c) { // Did we read an escape sequence? $c .= fread($inputStream, 2); @@ -270,7 +270,7 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)))); $output->write($remainingCharacters); $fullChoice .= $remainingCharacters; - $i = \strlen($fullChoice); + $i = self::strlen($fullChoice); $matches = array_filter( $autocomplete($ret), @@ -286,6 +286,8 @@ function ($match) use ($ret) { $output->write($c); break; } + + $numMatches = 0; } continue; diff --git a/src/Symfony/Component/Console/Input/ArrayInput.php b/src/Symfony/Component/Console/Input/ArrayInput.php index 5b1b2ed2165a3..66f0bedc3626b 100644 --- a/src/Symfony/Component/Console/Input/ArrayInput.php +++ b/src/Symfony/Component/Console/Input/ArrayInput.php @@ -39,8 +39,8 @@ public function __construct(array $parameters, InputDefinition $definition = nul */ public function getFirstArgument() { - foreach ($this->parameters as $key => $value) { - if ($key && '-' === $key[0]) { + foreach ($this->parameters as $param => $value) { + if ($param && \is_string($param) && '-' === $param[0]) { continue; } @@ -107,7 +107,7 @@ public function __toString() { $params = []; foreach ($this->parameters as $param => $val) { - if ($param && '-' === $param[0]) { + if ($param && \is_string($param) && '-' === $param[0]) { if (\is_array($val)) { foreach ($val as $v) { $params[] = $param.('' != $v ? '='.$this->escapeToken($v) : ''); diff --git a/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php new file mode 100644 index 0000000000000..33d5c3840f3e3 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Descriptor/ApplicationDescriptionTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Descriptor; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Descriptor\ApplicationDescription; + +final class ApplicationDescriptionTest extends TestCase +{ + /** + * @dataProvider getNamespacesProvider + */ + public function testGetNamespaces(array $expected, array $names) + { + $application = new TestApplication(); + foreach ($names as $name) { + $application->add(new Command($name)); + } + + $this->assertSame($expected, array_keys((new ApplicationDescription($application))->getNamespaces())); + } + + public function getNamespacesProvider() + { + return [ + [['_global'], ['foobar']], + [['a', 'b'], ['b:foo', 'a:foo', 'b:bar']], + [['_global', 'b', 'z', 22, 33], ['z:foo', '1', '33:foo', 'b:foo', '22:foo:bar']], + ]; + } +} + +final class TestApplication extends Application +{ + /** + * {@inheritdoc} + */ + protected function getDefaultCommands() + { + return []; + } +} diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php index 42e0a28530b23..fcba3b3b2fd19 100644 --- a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php @@ -189,19 +189,20 @@ public function testAskWithAutocomplete() // Acm // AcsTest // - // - // Test + // + // Test // // S // F00oo - $inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\nS\177\177\033[B\033[B\nF00\177\177oo\t\n"); + // F⭐ + $inputStream = $this->getInputStream("Acm\nAc\177\177s\tTest\n\n\033[A\033[A\033[A\n\033[A\033[A\033[A\033[A\033[A\033[A\033[A\tTest\n\033[B\nS\177\177\033[B\033[B\nF00\177\177oo\t\nF⭐\t\177\177⭐\t\n"); $dialog = new QuestionHelper(); $helperSet = new HelperSet([new FormatterHelper()]); $dialog->setHelperSet($helperSet); $question = new Question('Please select a bundle', 'FrameworkBundle'); - $question->setAutocompleterValues(['AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle']); + $question->setAutocompleterValues(['AcmeDemoBundle', 'AsseticBundle', 'SecurityBundle', 'FooBundle', 'F⭐Y']); $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); $this->assertEquals('AsseticBundleTest', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); @@ -211,6 +212,7 @@ public function testAskWithAutocomplete() $this->assertEquals('AcmeDemoBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); $this->assertEquals('AsseticBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); $this->assertEquals('FooBundle', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); + $this->assertEquals('F⭐Y', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); } public function testAskWithAutocompleteTrimmable() @@ -772,7 +774,7 @@ public function testTraversableMultiselectAutocomplete() // F00o,A,SecurityBundle // Acme,As<29x BACKSPACE>S // Ac,As<3x BACKSPACE>d - $inputStream = $this->getInputStream("\nF\t\nA\033[A\033[A\033[A\t,F\t\nF00\177\177o\t,A\033[B\t, SecurityBundle\nSecurityBundle\nAcme\t, As\t\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177S\t\nAc\t,As\t\177\177\177d\t\n"); + $inputStream = $this->getInputStream("\nF\t\nA\033[A\033[A\033[A\t,F\t\nF00\177\177o\t,A\033[B\t, SecurityBundle\nAcme\t, As\t\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177S\t\nAc\t,As\t\177\177\177d\t\n"); $dialog = new QuestionHelper(); $helperSet = new HelperSet([new FormatterHelper()]); diff --git a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php index 1a2518812fa22..d1d5f6d8a4e17 100644 --- a/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php +++ b/src/Symfony/Component/DependencyInjection/Argument/TaggedIteratorArgument.php @@ -21,6 +21,7 @@ class TaggedIteratorArgument extends IteratorArgument private $tag; private $indexAttribute; private $defaultIndexMethod; + private $defaultPriorityMethod; private $needsIndexes = false; /** diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php index 2147d53f1263d..161cd5960ca91 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidParameterTypeException; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -37,16 +38,14 @@ final class CheckTypeDeclarationsPass extends AbstractRecursivePass private const SCALAR_TYPES = ['int', 'float', 'bool', 'string']; private $autoload; - private $ignoredServices; /** * @param bool $autoload Whether services who's class in not loaded should be checked or not. * Defaults to false to save loading code during compilation. */ - public function __construct(bool $autoload = false, array $ignoredServices = []) + public function __construct(bool $autoload = false) { $this->autoload = $autoload; - $this->ignoredServices = array_flip($ignoredServices); } /** @@ -54,7 +53,7 @@ public function __construct(bool $autoload = false, array $ignoredServices = []) */ protected function processValue($value, $isRoot = false) { - if (!$value instanceof Definition || isset($this->ignoredServices[$this->currentId])) { + if (!$value instanceof Definition || $value->hasErrors()) { return parent::processValue($value, $isRoot); } @@ -71,7 +70,15 @@ protected function processValue($value, $isRoot = false) } foreach ($value->getMethodCalls() as $methodCall) { - $reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]); + try { + $reflectionMethod = $this->getReflectionMethod($value, $methodCall[0]); + } catch (RuntimeException $e) { + if ($value->getFactory()) { + continue; + } + + throw $e; + } $this->checkTypeDeclarations($value, $reflectionMethod, $methodCall[1]); } @@ -152,6 +159,10 @@ private function checkType(Definition $checkedDefinition, $value, \ReflectionPar return; } + if ('object' === $type) { + return; + } + if (is_a($class, $type, true)) { return; } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index dd3a1edf0199d..fb5d827acdc0a 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -316,6 +316,11 @@ private function convertParameters(array $parameters, string $type, \DOMElement if (\in_array($value, ['null', 'true', 'false'], true)) { $element->setAttribute('type', 'string'); } + + if (\is_string($value) && (is_numeric($value) || preg_match('/^0b[01]*$/', $value) || preg_match('/^0x[0-9a-f]++$/i', $value))) { + $element->setAttribute('type', 'string'); + } + $text = $this->document->createTextNode(self::phpToXml($value)); $element->appendChild($text); } diff --git a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php index c42e22addfe95..1b615c758eaee 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/FileLoader.php @@ -36,6 +36,7 @@ abstract class FileLoader extends BaseFileLoader protected $instanceof = []; protected $interfaces = []; protected $singlyImplemented = []; + protected $autoRegisterAliasesForSinglyImplementedInterfaces = true; public function __construct(ContainerBuilder $container, FileLocatorInterface $locator) { @@ -114,12 +115,16 @@ public function registerClasses(Definition $prototype, $namespace, $resource, $e } } } + + if ($this->autoRegisterAliasesForSinglyImplementedInterfaces) { + $this->registerAliasesForSinglyImplementedInterfaces(); + } } public function registerAliasesForSinglyImplementedInterfaces() { foreach ($this->interfaces as $interface) { - if (!empty($this->singlyImplemented[$interface]) && !$this->container->hasAlias($interface)) { + if (!empty($this->singlyImplemented[$interface]) && !$this->container->has($interface)) { $this->container->setAlias($interface, $this->singlyImplemented[$interface])->setPublic(false); } } diff --git a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php index db117d25b8688..207fb83ec180f 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/PhpFileLoader.php @@ -23,6 +23,8 @@ */ class PhpFileLoader extends FileLoader { + protected $autoRegisterAliasesForSinglyImplementedInterfaces = false; + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php index 755cf6b07be8f..82e61b6335ba1 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php @@ -36,6 +36,8 @@ class XmlFileLoader extends FileLoader { const NS = 'http://symfony.com/schema/dic/services'; + protected $autoRegisterAliasesForSinglyImplementedInterfaces = false; + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php index 11dd1e312a6c4..cb17e5743d59a 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php +++ b/src/Symfony/Component/DependencyInjection/Loader/YamlFileLoader.php @@ -110,6 +110,8 @@ class YamlFileLoader extends FileLoader private $anonymousServicesCount; private $anonymousServicesSuffix; + protected $autoRegisterAliasesForSinglyImplementedInterfaces = false; + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd index 4f85f41035d8e..2f745c3326d49 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd +++ b/src/Symfony/Component/DependencyInjection/Loader/schema/dic/services/services-1.0.xsd @@ -222,6 +222,7 @@ + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php index 51bc7c6779d20..b012a65625610 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php @@ -15,12 +15,14 @@ use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Bar; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgument; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgumentNotNull; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Foo; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\FooObject; /** * @author Nicolas Grekas @@ -390,6 +392,21 @@ public function testProcessSuccessWhenPassingAnIteratorArgumentToIterable() $this->addToAssertionCount(1); } + /** + * @requires PHP 7.2 + */ + public function testProcessSuccessWhenPassingDefinitionForObjectType() + { + $container = new ContainerBuilder(); + + $container->register('foo_object', FooObject::class) + ->addArgument(new Definition(Foo::class)); + + (new CheckTypeDeclarationsPass(true))->process($container); + + $this->addToAssertionCount(1); + } + public function testProcessFactory() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php index 8ea5101e6e86f..10d684a7691fa 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php @@ -19,7 +19,6 @@ use Symfony\Component\DependencyInjection\Compiler\ResolveBindingsPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; -use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php index 1aaca2f1560c9..7e1a30b5ffa07 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Bar.php @@ -15,7 +15,7 @@ class Bar implements BarInterface { public $quz; - public function __construct($quz = null, \NonExistent $nonExistent = null, BarInterface $decorated = null, array $foo = []) + public function __construct($quz = null, \NonExistent $nonExistent = null, BarInterface $decorated = null, array $foo = [], iterable $baz = []) { $this->quz = $quz; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/FooObject.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/FooObject.php new file mode 100644 index 0000000000000..44cfaf0d0a3c0 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/FooObject.php @@ -0,0 +1,10 @@ + [true, false, null, 0, 1000.3, 'true', 'false', 'null'], 'binary' => "\xf0\xf0\xf0\xf0", 'binary-control-char' => "This is a Bell char \x07", + 'null string' => 'null', + 'string of digits' => '123', + 'string of digits prefixed with minus character' => '-123', + 'true string' => 'true', + 'false string' => 'false', + 'binary number string' => '0b0110', + 'numeric string' => '-1.2E2', + 'hexadecimal number string' => '0xFF', + 'float string' => '10100.1', + 'positive float string' => '+10100.1', + 'negative float string' => '-10100.1', ])); return $container; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php index d8b3fd4b903d2..76ddd0d3b84cb 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php @@ -108,6 +108,17 @@ protected function getDefaultParameters(): array ], 'binary' => '', 'binary-control-char' => 'This is a Bell char ', + 'null string' => 'null', + 'string of digits' => '123', + 'string of digits prefixed with minus character' => '-123', + 'true string' => 'true', + 'false string' => 'false', + 'binary number string' => '0b0110', + 'numeric string' => '-1.2E2', + 'hexadecimal number string' => '0xFF', + 'float string' => '10100.1', + 'positive float string' => '+10100.1', + 'negative float string' => '-10100.1', ]; } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml index c464d9ccff68c..d817a079a08ed 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services8.xml @@ -20,6 +20,17 @@ 8PDw8A== VGhpcyBpcyBhIEJlbGwgY2hhciAH + null + 123 + -123 + true + false + 0b0110 + -1.2E2 + 0xFF + 10100.1 + +10100.1 + -10100.1 diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_bindings.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_bindings.xml index 8e4486c0ba67e..e111af4fcee26 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_bindings.xml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_bindings.xml @@ -12,6 +12,7 @@ null + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml index a17a71cc8ecf5..8c9f7a2902fc2 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services8.yml @@ -6,6 +6,17 @@ parameters: values: [true, false, null, 0, 1000.3, 'true', 'false', 'null'] binary: !!binary 8PDw8A== binary-control-char: !!binary VGhpcyBpcyBhIEJlbGwgY2hhciAH + null string: 'null' + string of digits: '123' + string of digits prefixed with minus character: '-123' + true string: 'true' + false string: 'false' + binary number string: '0b0110' + numeric string: '-1.2E2' + hexadecimal number string: '0xFF' + float string: '10100.1' + positive float string: '+10100.1' + negative float string: '-10100.1' services: service_container: diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml index 0eba120b586e2..6b3b3607a9e05 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_bindings.yml @@ -12,6 +12,7 @@ services: bind: Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface: '@Symfony\Component\DependencyInjection\Tests\Fixtures\Bar' $foo: [ ~ ] + iterable $baz: !tagged_iterator { tag: bar } Symfony\Component\DependencyInjection\Tests\Fixtures\Bar: factory: [ ~, 'create' ] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php index d010bc71ee7de..0302981f0f9e2 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/FileLoaderTest.php @@ -89,6 +89,7 @@ public function testRegisterClasses() $container = new ContainerBuilder(); $container->setParameter('sub_dir', 'Sub'); $loader = new TestFileLoader($container, new FileLocator(self::$fixturesPath.'/Fixtures')); + $loader->autoRegisterAliasesForSinglyImplementedInterfaces = false; $loader->registerClasses(new Definition(), 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\\', 'Prototype/%sub_dir%/*'); $loader->registerClasses(new Definition(), 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\Sub\\', 'Prototype/%sub_dir%/*'); // loading twice should not be an issue @@ -121,7 +122,6 @@ public function testRegisterClassesWithExclude() // load everything, except OtherDir/AnotherSub & Foo.php 'Prototype/{%other_dir%/AnotherSub,Foo.php}' ); - $loader->registerAliasesForSinglyImplementedInterfaces(); $this->assertTrue($container->has(Bar::class)); $this->assertTrue($container->has(Baz::class)); @@ -151,7 +151,6 @@ public function testRegisterClassesWithExcludeAsArray() 'Prototype/OtherDir/AnotherSub/DeeperBaz.php', ] ); - $loader->registerAliasesForSinglyImplementedInterfaces(); $this->assertTrue($container->has(Foo::class)); $this->assertTrue($container->has(Baz::class)); @@ -167,7 +166,6 @@ public function testNestedRegisterClasses() $prototype = new Definition(); $prototype->setPublic(true)->setPrivate(true); $loader->registerClasses($prototype, 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', 'Prototype/*'); - $loader->registerAliasesForSinglyImplementedInterfaces(); $this->assertTrue($container->has(Bar::class)); $this->assertTrue($container->has(Baz::class)); @@ -199,7 +197,6 @@ public function testMissingParentClass() 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\BadClasses\\', 'Prototype/%bad_classes_dir%/*' ); - $loader->registerAliasesForSinglyImplementedInterfaces(); $this->assertTrue($container->has(MissingParent::class)); @@ -218,7 +215,6 @@ public function testRegisterClassesWithBadPrefix() // the Sub is missing from namespace prefix $loader->registerClasses(new Definition(), 'Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype\\', 'Prototype/Sub/*'); - $loader->registerAliasesForSinglyImplementedInterfaces(); } public function testRegisterClassesWithIncompatibleExclude() @@ -234,12 +230,13 @@ public function testRegisterClassesWithIncompatibleExclude() 'Prototype/*', 'yaml/*' ); - $loader->registerAliasesForSinglyImplementedInterfaces(); } } class TestFileLoader extends FileLoader { + public $autoRegisterAliasesForSinglyImplementedInterfaces = true; + public function load($resource, string $type = null) { return $resource; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index 2c5386572a439..14ba4c7f6e567 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -879,12 +879,14 @@ public function testBindings() '$foo' => [null], '$quz' => 'quz', '$factory' => 'factory', + 'iterable $baz' => new TaggedIteratorArgument('bar'), ], array_map(function (BoundArgument $v) { return $v->getValues()[0]; }, $definition->getBindings())); $this->assertEquals([ 'quz', null, new Reference(Bar::class), [null], + new TaggedIteratorArgument('bar'), ], $definition->getArguments()); $definition = $container->getDefinition(Bar::class); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 41115698d0b8b..d2e81db377313 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -374,6 +374,9 @@ public function testParsesIteratorArgument() $lazyDefinition = $container->getDefinition('lazy_context'); $this->assertEquals([new IteratorArgument(['k1' => new Reference('foo.baz'), 'k2' => new Reference('service_container')]), new IteratorArgument([])], $lazyDefinition->getArguments(), '->load() parses lazy arguments'); + + $message = 'The "deprecated_service" service is deprecated. You should stop using it, as it will be removed in the future.'; + $this->assertSame($message, $container->getDefinition('deprecated_service')->getDeprecationMessage('deprecated_service')); } public function testAutowire() @@ -766,12 +769,14 @@ public function testBindings() '$foo' => [null], '$quz' => 'quz', '$factory' => 'factory', + 'iterable $baz' => new TaggedIteratorArgument('bar'), ], array_map(function (BoundArgument $v) { return $v->getValues()[0]; }, $definition->getBindings())); $this->assertEquals([ 'quz', null, new Reference(Bar::class), [null], + new TaggedIteratorArgument('bar'), ], $definition->getArguments()); $definition = $container->getDefinition(Bar::class); diff --git a/src/Symfony/Component/Dotenv/Dotenv.php b/src/Symfony/Component/Dotenv/Dotenv.php index 617f4ba00cf96..38fb908897fee 100644 --- a/src/Symfony/Component/Dotenv/Dotenv.php +++ b/src/Symfony/Component/Dotenv/Dotenv.php @@ -461,7 +461,7 @@ private function resolveVariables(string $value, array $loadedVars): string $value = ''; } - if ('' === $value && isset($matches['default_value'])) { + if ('' === $value && isset($matches['default_value']) && '' !== $matches['default_value']) { $unsupportedChars = strpbrk($matches['default_value'], '\'"{$'); if (false !== $unsupportedChars) { throw $this->createFormatException(sprintf('Unsupported character "%s" found in the default value of variable "$%s".', $unsupportedChars[0], $name)); diff --git a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php index ed54d2a689919..489c7fa6d1933 100644 --- a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php +++ b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php @@ -172,6 +172,7 @@ public function getEnvData() ["FOO=BAR\nBAR=\${NOTDEFINED:=TEST}", ['FOO' => 'BAR', 'NOTDEFINED' => 'TEST', 'BAR' => 'TEST']], ["FOO=\nBAR=\${FOO:=TEST}", ['FOO' => 'TEST', 'BAR' => 'TEST']], ["FOO=\nBAR=\$FOO:=TEST}", ['FOO' => 'TEST', 'BAR' => 'TEST}']], + ["FOO=foo\nFOOBAR=\${FOO}\${BAR}", ['FOO' => 'foo', 'FOOBAR' => 'foo']], ]; if ('\\' !== \DIRECTORY_SEPARATOR) { diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php index 585df7408e7f5..7ab4c00bd3f33 100644 --- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -399,7 +399,6 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array if ( 'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7' === $class || 'Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV6' === $class - || 'Test\Symfony\Component\Debug\Tests' === $refl->getNamespaceName() ) { return []; } @@ -724,7 +723,11 @@ private function darwinRealpath(string $real): string $real = self::$darwinCache[$kDir][0]; } else { $dir = getcwd(); - chdir($real); + + if (!@chdir($real)) { + return $real.$file; + } + $real = getcwd().'/'; chdir($dir); diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php index 84cb9da42413f..883a94f68968f 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/HtmlErrorRenderer.php @@ -36,16 +36,28 @@ class HtmlErrorRenderer implements ErrorRendererInterface private $charset; private $fileLinkFormat; private $projectDir; - private $requestStack; + private $outputBuffer; private $logger; - public function __construct(bool $debug = false, string $charset = null, $fileLinkFormat = null, string $projectDir = null, RequestStack $requestStack = null, LoggerInterface $logger = null) + /** + * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it + * @param bool|callable $outputBuffer The output buffer as a string or a callable that should return it + */ + public function __construct($debug = false, string $charset = null, $fileLinkFormat = null, string $projectDir = null, $outputBuffer = '', LoggerInterface $logger = null) { + if (!\is_bool($debug) && !\is_callable($debug)) { + throw new \TypeError(sprintf('Argument 1 passed to %s() must be a boolean or a callable, %s given.', __METHOD__, \is_object($debug) ? \get_class($debug) : \gettype($debug))); + } + + if (!\is_string($outputBuffer) && !\is_callable($outputBuffer)) { + throw new \TypeError(sprintf('Argument 5 passed to %s() must be a string or a callable, %s given.', __METHOD__, \is_object($outputBuffer) ? \get_class($outputBuffer) : \gettype($outputBuffer))); + } + $this->debug = $debug; $this->charset = $charset ?: (ini_get('default_charset') ?: 'UTF-8'); $this->fileLinkFormat = $fileLinkFormat ?: (ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format')); $this->projectDir = $projectDir; - $this->requestStack = $requestStack; + $this->outputBuffer = $outputBuffer; $this->logger = $logger; } @@ -57,7 +69,7 @@ public function render(\Throwable $exception): FlattenException $exception = FlattenException::createFromThrowable($exception, null, [ 'Content-Type' => 'text/html; charset='.$this->charset, ]); - + return $exception->setAsString($this->renderException($exception)); } @@ -81,12 +93,43 @@ public function getStylesheet(): string return $this->include('assets/css/exception.css'); } + public static function isDebug(RequestStack $requestStack, bool $debug): \Closure + { + return static function () use ($requestStack, $debug): bool { + if (!$request = $requestStack->getCurrentRequest()) { + return $debug; + } + + return $debug && $request->attributes->getBoolean('showException', true); + }; + } + + public static function getAndCleanOutputBuffer(RequestStack $requestStack): \Closure + { + return static function () use ($requestStack): string { + if (!$request = $requestStack->getCurrentRequest()) { + return ''; + } + + $startObLevel = $request->headers->get('X-Php-Ob-Level', -1); + + if (ob_get_level() <= $startObLevel) { + return ''; + } + + Response::closeOutputBuffers($startObLevel + 1, true); + + return ob_get_clean(); + }; + } + private function renderException(FlattenException $exception, string $debugTemplate = 'views/exception_full.html.php'): string { + $debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($exception); $statusText = $this->escape($exception->getStatusText()); $statusCode = $this->escape($exception->getStatusCode()); - if (!$this->debug) { + if (!$debug) { return $this->include('views/error.html.php', [ 'statusText' => $statusText, 'statusCode' => $statusCode, @@ -94,7 +137,6 @@ private function renderException(FlattenException $exception, string $debugTempl } $exceptionMessage = $this->escape($exception->getMessage()); - $request = $this->requestStack ? $this->requestStack->getCurrentRequest() : null; return $this->include($debugTemplate, [ 'exception' => $exception, @@ -102,21 +144,10 @@ private function renderException(FlattenException $exception, string $debugTempl 'statusText' => $statusText, 'statusCode' => $statusCode, 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, - 'currentContent' => $request ? $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level', -1)) : '', + 'currentContent' => \is_string($this->outputBuffer) ? $this->outputBuffer : ($this->outputBuffer)(), ]); } - private function getAndCleanOutputBuffering(int $startObLevel): string - { - if (ob_get_level() <= $startObLevel) { - return ''; - } - - Response::closeOutputBuffers($startObLevel + 1, true); - - return ob_get_clean(); - } - /** * Formats an array as a string. */ @@ -312,7 +343,7 @@ private function include(string $name, array $context = []): string { extract($context, EXTR_SKIP); ob_start(); - include __DIR__ . '/../Resources/' .$name; + include __DIR__.'/../Resources/'.$name; return trim(ob_get_clean()); } diff --git a/src/Symfony/Component/ErrorHandler/ErrorRenderer/SerializerErrorRenderer.php b/src/Symfony/Component/ErrorHandler/ErrorRenderer/SerializerErrorRenderer.php index c055bc3b65db3..6cc363d0d9f1e 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorRenderer/SerializerErrorRenderer.php +++ b/src/Symfony/Component/ErrorHandler/ErrorRenderer/SerializerErrorRenderer.php @@ -26,19 +26,26 @@ class SerializerErrorRenderer implements ErrorRendererInterface private $serializer; private $format; private $fallbackErrorRenderer; + private $debug; /** * @param string|callable(FlattenException) $format The format as a string or a callable that should return it + * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it */ - public function __construct(SerializerInterface $serializer, $format, ErrorRendererInterface $fallbackErrorRenderer = null) + public function __construct(SerializerInterface $serializer, $format, ErrorRendererInterface $fallbackErrorRenderer = null, $debug = false) { if (!\is_string($format) && !\is_callable($format)) { throw new \TypeError(sprintf('Argument 2 passed to %s() must be a string or a callable, %s given.', __METHOD__, \is_object($format) ? \get_class($format) : \gettype($format))); } + if (!\is_bool($debug) && !\is_callable($debug)) { + throw new \TypeError(sprintf('Argument 4 passed to %s() must be a boolean or a callable, %s given.', __METHOD__, \is_object($debug) ? \get_class($debug) : \gettype($debug))); + } + $this->serializer = $serializer; $this->format = $format; $this->fallbackErrorRenderer = $fallbackErrorRenderer ?? new HtmlErrorRenderer(); + $this->debug = $debug; } /** @@ -51,7 +58,10 @@ public function render(\Throwable $exception): FlattenException try { $format = \is_string($this->format) ? $this->format : ($this->format)($flattenException); - return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, ['exception' => $exception])); + return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, [ + 'exception' => $exception, + 'debug' => \is_bool($this->debug) ? $this->debug : ($this->debug)($exception), + ])); } catch (NotEncodableValueException $e) { return $this->fallbackErrorRenderer->render($exception); } diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php index b140ca6e52f74..f292d0f79618f 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/HtmlErrorRendererTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; -use Symfony\Component\ErrorHandler\Exception\FlattenException; class HtmlErrorRendererTest extends TestCase { diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php index a1698e0a88cd9..77a28ecde69d6 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorRenderer/SerializerErrorRendererTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer; -use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ProblemNormalizer; use Symfony\Component\Serializer\Serializer; diff --git a/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php b/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php index 5db49b88fc739..cceb144b123e6 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/Exception/FlattenExceptionTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\ErrorHandler\Tests\Exception; use PHPUnit\Framework\TestCase; -use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index bb1484ab67798..d4299da4d93f3 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -783,7 +783,7 @@ public function testSymlink() $file = $this->workspace.\DIRECTORY_SEPARATOR.'file'; $link = $this->workspace.\DIRECTORY_SEPARATOR.'link'; - // $file does not exists right now: creating "broken" links is a wanted feature + // $file does not exist right now: creating "broken" links is a wanted feature $this->filesystem->symlink($file, $link); $this->assertTrue(is_link($link)); diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index b4bac3858cab7..1b184b6ab4ccf 100644 --- a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php +++ b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php @@ -105,6 +105,14 @@ public function createView(ChoiceListInterface $list, $preferredChoices = null, unset($otherViews[$key]); } } + + foreach ($preferredViewsOrder as $key => $groupViewsOrder) { + if ($groupViewsOrder) { + $preferredViewsOrder[$key] = min($groupViewsOrder); + } else { + unset($preferredViewsOrder[$key]); + } + } } else { // Otherwise use the original structure of the choices self::addChoiceViewsFromStructuredValues( @@ -249,6 +257,9 @@ private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choi $preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel); $otherViews[$groupLabel] = new ChoiceGroupView($groupLabel); } + if (!isset($preferredViewsOrder[$groupLabel])) { + $preferredViewsOrder[$groupLabel] = []; + } self::addChoiceView( $choice, @@ -259,7 +270,7 @@ private static function addChoiceViewsGroupedByCallable(callable $groupBy, $choi $attr, $isPreferred, $preferredViews[$groupLabel]->choices, - $preferredViewsOrder, + $preferredViewsOrder[$groupLabel], $otherViews[$groupLabel]->choices ); } diff --git a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php index 4472bd06c9403..7c717f441b426 100644 --- a/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php +++ b/src/Symfony/Component/Form/Tests/ChoiceList/Factory/DefaultChoiceListFactoryTest.php @@ -256,6 +256,37 @@ public function testCreateViewFlatPreferredChoicesSameOrder() ); } + public function testCreateViewFlatPreferredChoiceGroupsSameOrder() + { + $view = $this->factory->createView( + $this->list, + [$this->obj4, $this->obj2, $this->obj1, $this->obj3], + null, // label + null, // index + [$this, 'getGroup'] + ); + + $preferredLabels = array_map(static function (ChoiceGroupView $groupView): array { + return array_map(static function (ChoiceView $view): string { + return $view->label; + }, $groupView->choices); + }, $view->preferredChoices); + + $this->assertEquals( + [ + 'Group 2' => [ + 2 => 'C', + 3 => 'D', + ], + 'Group 1' => [ + 0 => 'A', + 1 => 'B', + ], + ], + $preferredLabels + ); + } + public function testCreateViewFlatPreferredChoicesEmptyArray() { $view = $this->factory->createView( diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 5e9fe4221beb7..0f872dfa97175 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -23,6 +23,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; use Symfony\Contracts\HttpClient\ResponseStreamInterface; +use Symfony\Contracts\Service\ResetInterface; /** * A performant implementation of the HttpClientInterface contracts based on the curl extension. @@ -32,7 +33,7 @@ * * @author Nicolas Grekas */ -final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface +final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface { use HttpClientTrait; use LoggerAwareTrait; @@ -324,9 +325,17 @@ public function stream($responses, float $timeout = null): ResponseStreamInterfa return new ResponseStream(CurlResponse::stream($responses, $timeout)); } - public function __destruct() + public function reset() { + if ($this->logger) { + foreach ($this->multi->pushedResponses as $url => $response) { + $this->logger->debug(sprintf('Unused pushed response: "%s"', $url)); + } + } + $this->multi->pushedResponses = []; + $this->multi->dnsCache->evictions = $this->multi->dnsCache->evictions ?: $this->multi->dnsCache->removals; + $this->multi->dnsCache->removals = $this->multi->dnsCache->hostnames = []; if (\is_resource($this->multi->handle)) { if (\defined('CURLMOPT_PUSHFUNCTION')) { @@ -344,6 +353,11 @@ public function __destruct() } } + public function __destruct() + { + $this->reset(); + } + private static function handlePush($parent, $pushed, array $requestHeaders, CurlClientState $multi, int $maxPendingPushes, ?LoggerInterface $logger): int { $headers = []; @@ -363,12 +377,6 @@ private static function handlePush($parent, $pushed, array $requestHeaders, Curl $url = $headers[':scheme'][0].'://'.$headers[':authority'][0]; - if ($maxPendingPushes <= \count($multi->pushedResponses)) { - $logger && $logger->debug(sprintf('Rejecting pushed response from "%s" for "%s": the queue is full', $origin, $url)); - - return CURL_PUSH_DENY; - } - // curl before 7.65 doesn't validate the pushed ":authority" header, // but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host, // ignoring domains mentioned as alt-name in the certificate for now (same as curl). @@ -378,6 +386,12 @@ private static function handlePush($parent, $pushed, array $requestHeaders, Curl return CURL_PUSH_DENY; } + if ($maxPendingPushes <= \count($multi->pushedResponses)) { + $fifoUrl = key($multi->pushedResponses); + unset($multi->pushedResponses[$fifoUrl]); + $logger && $logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl)); + } + $url .= $headers[':path'][0]; $logger && $logger->debug(sprintf('Queueing pushed response: "%s"', $url)); diff --git a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php index 948b2d8eae89c..fb9228b10610d 100644 --- a/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php +++ b/src/Symfony/Component/HttpClient/DataCollector/HttpClientDataCollector.php @@ -15,11 +15,12 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; /** * @author Jérémy Romey */ -final class HttpClientDataCollector extends DataCollector +final class HttpClientDataCollector extends DataCollector implements LateDataCollectorInterface { /** * @var TraceableHttpClient[] @@ -36,7 +37,7 @@ public function registerClient(string $name, TraceableHttpClient $client) */ public function collect(Request $request, Response $response, \Throwable $exception = null) { - $this->initData(); + $this->reset(); foreach ($this->clients as $name => $client) { [$errorCount, $traces] = $this->collectOnClient($client); @@ -51,6 +52,13 @@ public function collect(Request $request, Response $response, \Throwable $except } } + public function lateCollect() + { + foreach ($this->clients as $client) { + $client->reset(); + } + } + public function getClients(): array { return $this->data['clients'] ?? []; @@ -66,17 +74,6 @@ public function getErrorCount(): int return $this->data['error_count'] ?? 0; } - /** - * {@inheritdoc} - */ - public function reset() - { - $this->initData(); - foreach ($this->clients as $client) { - $client->reset(); - } - } - /** * {@inheritdoc} */ @@ -85,7 +82,7 @@ public function getName(): string return 'http_client'; } - private function initData() + public function reset() { $this->data = [ 'clients' => [], diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index e06f9a58bab08..618858728839f 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -228,15 +228,7 @@ public function __destruct() } finally { $this->close(); - // Clear local caches when the only remaining handles are about pushed responses if (!$this->multi->openHandles) { - if ($this->logger) { - foreach ($this->multi->pushedResponses as $url => $response) { - $this->logger->debug(sprintf('Unused pushed response: "%s"', $url)); - } - } - - $this->multi->pushedResponses = []; // Schedule DNS cache eviction for the next request $this->multi->dnsCache->evictions = $this->multi->dnsCache->evictions ?: $this->multi->dnsCache->removals; $this->multi->dnsCache->removals = $this->multi->dnsCache->hostnames = []; diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index 2e96bd411d589..2e8bce58af198 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -113,11 +113,18 @@ public function __destruct() private function open(): void { - set_error_handler(function ($type, $msg) { throw new TransportException($msg); }); + $url = $this->url; + + set_error_handler(function ($type, $msg) use (&$url) { + if (E_NOTICE !== $type || 'fopen(): Content-type not specified assuming application/x-www-form-urlencoded' !== $msg) { + throw new TransportException($msg); + } + + $this->logger && $this->logger->info(sprintf('%s for "%s".', $msg, $url ?? $this->url)); + }); try { $this->info['start_time'] = microtime(true); - $url = $this->url; while (true) { $context = stream_context_get_options($this->context); diff --git a/src/Symfony/Component/HttpClient/ScopingHttpClient.php b/src/Symfony/Component/HttpClient/ScopingHttpClient.php index 3f071720f0573..a55d011953086 100644 --- a/src/Symfony/Component/HttpClient/ScopingHttpClient.php +++ b/src/Symfony/Component/HttpClient/ScopingHttpClient.php @@ -15,13 +15,14 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; use Symfony\Contracts\HttpClient\ResponseStreamInterface; +use Symfony\Contracts\Service\ResetInterface; /** * Auto-configure the default options based on the requested URL. * * @author Anthony Martin */ -class ScopingHttpClient implements HttpClientInterface +class ScopingHttpClient implements HttpClientInterface, ResetInterface { use HttpClientTrait; @@ -90,4 +91,11 @@ public function stream($responses, float $timeout = null): ResponseStreamInterfa { return $this->client->stream($responses, $timeout); } + + public function reset() + { + if ($this->client instanceof ResetInterface) { + $this->client->reset(); + } + } } diff --git a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php index 4fd367fd9d169..f83edf91b6f39 100644 --- a/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php @@ -13,13 +13,23 @@ use Psr\Log\AbstractLogger; use Symfony\Component\HttpClient\CurlHttpClient; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; use Symfony\Contracts\HttpClient\HttpClientInterface; +/* +Tests for HTTP2 Push need a recent version of both PHP and curl. This docker command should run them: +docker run -it --rm -v $(pwd):/app -v /path/to/vulcain:/usr/local/bin/vulcain -w /app php:7.3-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push +The vulcain binary can be found at https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz - see https://github.com/dunglas/vulcain for source +*/ + /** * @requires extension curl */ class CurlHttpClientTest extends HttpClientTestCase { + private static $vulcainStarted = false; + protected function getHttpClient(string $testCase): HttpClientInterface { return new CurlHttpClient(); @@ -28,7 +38,81 @@ protected function getHttpClient(string $testCase): HttpClientInterface /** * @requires PHP 7.2.17 */ - public function testHttp2Push() + public function testHttp2PushVulcain() + { + $client = $this->getVulcainClient(); + $logger = new TestLogger(); + $client->setLogger($logger); + + $responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [ + 'headers' => [ + 'Preload' => '/documents/*/id', + ], + ])->toArray(); + + foreach ($responseAsArray['documents'] as $document) { + $client->request('GET', 'https://127.0.0.1:3000'.$document['id'])->toArray(); + } + + $client->reset(); + + $expected = [ + 'Request: "GET https://127.0.0.1:3000/json"', + 'Queueing pushed response: "https://127.0.0.1:3000/json/1"', + 'Queueing pushed response: "https://127.0.0.1:3000/json/2"', + 'Queueing pushed response: "https://127.0.0.1:3000/json/3"', + 'Response: "200 https://127.0.0.1:3000/json"', + 'Accepting pushed response: "GET https://127.0.0.1:3000/json/1"', + 'Response: "200 https://127.0.0.1:3000/json/1"', + 'Accepting pushed response: "GET https://127.0.0.1:3000/json/2"', + 'Response: "200 https://127.0.0.1:3000/json/2"', + 'Accepting pushed response: "GET https://127.0.0.1:3000/json/3"', + 'Response: "200 https://127.0.0.1:3000/json/3"', + ]; + $this->assertSame($expected, $logger->logs); + } + + /** + * @requires PHP 7.2.17 + */ + public function testHttp2PushVulcainWithUnusedResponse() + { + $client = $this->getVulcainClient(); + $logger = new TestLogger(); + $client->setLogger($logger); + + $responseAsArray = $client->request('GET', 'https://127.0.0.1:3000/json', [ + 'headers' => [ + 'Preload' => '/documents/*/id', + ], + ])->toArray(); + + $i = 0; + foreach ($responseAsArray['documents'] as $document) { + $client->request('GET', 'https://127.0.0.1:3000'.$document['id'])->toArray(); + if (++$i >= 2) { + break; + } + } + + $client->reset(); + + $expected = [ + 'Request: "GET https://127.0.0.1:3000/json"', + 'Queueing pushed response: "https://127.0.0.1:3000/json/1"', + 'Queueing pushed response: "https://127.0.0.1:3000/json/2"', + 'Queueing pushed response: "https://127.0.0.1:3000/json/3"', + 'Response: "200 https://127.0.0.1:3000/json"', + 'Accepting pushed response: "GET https://127.0.0.1:3000/json/1"', + 'Response: "200 https://127.0.0.1:3000/json/1"', + 'Accepting pushed response: "GET https://127.0.0.1:3000/json/2"', + 'Response: "200 https://127.0.0.1:3000/json/2"', + 'Unused pushed response: "https://127.0.0.1:3000/json/3"', + ]; + $this->assertSame($expected, $logger->logs); + } + + private function getVulcainClient(): CurlHttpClient { if (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) { $this->markTestSkipped('PHP 7.3.0 to 7.3.3 don\'t support HTTP/2 PUSH'); @@ -38,32 +122,44 @@ public function testHttp2Push() $this->markTestSkipped('curl <7.61 is used or it is not compiled with support for HTTP/2 PUSH'); } - $logger = new class() extends AbstractLogger { - public $logs = []; + $client = new CurlHttpClient(['verify_peer' => false, 'verify_host' => false]); - public function log($level, $message, array $context = []): void - { - $this->logs[] = $message; - } - }; + if (static::$vulcainStarted) { + return $client; + } - $client = new CurlHttpClient([], 6, 2); - $client->setLogger($logger); + if (['application/json'] !== $client->request('GET', 'http://127.0.0.1:8057/json')->getHeaders()['content-type']) { + $this->markTestSkipped('symfony/http-client-contracts >= 2.0.1 required'); + } - $index = $client->request('GET', 'https://http2.akamai.com/'); - $index->getContent(); + $process = new Process(['vulcain'], null, [ + 'DEBUG' => 1, + 'UPSTREAM' => 'http://127.0.0.1:8057', + 'ADDR' => ':3000', + 'KEY_FILE' => __DIR__.'/Fixtures/tls/server.key', + 'CERT_FILE' => __DIR__.'/Fixtures/tls/server.crt', + ]); + $process->start(); - $css = $client->request('GET', 'https://http2.akamai.com/resources/push.css'); + register_shutdown_function([$process, 'stop']); + sleep('\\' === \DIRECTORY_SEPARATOR ? 10 : 1); - $css->getHeaders(); + if (!$process->isRunning()) { + throw new ProcessFailedException($process); + } - $expected = [ - 'Request: "GET https://http2.akamai.com/"', - 'Queueing pushed response: "https://http2.akamai.com/resources/push.css"', - 'Response: "200 https://http2.akamai.com/"', - 'Accepting pushed response: "GET https://http2.akamai.com/resources/push.css"', - 'Response: "200 https://http2.akamai.com/resources/push.css"', - ]; - $this->assertSame($expected, $logger->logs); + static::$vulcainStarted = true; + + return $client; + } +} + +class TestLogger extends AbstractLogger +{ + public $logs = []; + + public function log($level, $message, array $context = []): void + { + $this->logs[] = $message; } } diff --git a/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.crt b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.crt new file mode 100644 index 0000000000000..3903667223308 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDPjCCAiYCCQDpVvfmCZt2GzANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV +UzEUMBIGA1UEBwwLR290aGFtIENpdHkxEjAQBgNVBAMMCWxvY2FsaG9zdDEoMCYG +CSqGSIb3DQEJARYZZHVuZ2xhcyttZXJjdXJlQGdtYWlsLmNvbTAeFw0xOTAxMjMx +NTUzMzlaFw0yOTAxMjAxNTUzMzlaMGExCzAJBgNVBAYTAlVTMRQwEgYDVQQHDAtH +b3RoYW0gQ2l0eTESMBAGA1UEAwwJbG9jYWxob3N0MSgwJgYJKoZIhvcNAQkBFhlk +dW5nbGFzK21lcmN1cmVAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAuKnXkBSJwOwkKfR58wP/yLYW9QFX2THoqN8iffangRmZwc5KLE6F +1S8jYMv3JGiJ95Ij3MezAfuBCdgPqqP8JrR1XwjR1RFZMOL/4U9R9OuMVng04PLw +L6TzKoEtZuExHUWFP0+5AYblgno2hoN/HVuox8m6zQrBNcbhTgDIjP5Hn491d9od +MtS3OxksDLr1UIOUGUWF7MQMN7lsN7rgT5qxoCkcAGAB4GPOA23HMt2zt4afDiI7 +lAmuv8MKkTmBCcFe+q+U7o6wMxkjGstzAWRibtwzR4ejPwdO7se23MXCWGPvF16Z +tu1ip+e+waRus9o5UnyGaVPFAw8iCTC/KwIDAQABMA0GCSqGSIb3DQEBCwUAA4IB +AQB42AW7E57yOky8GpsKLoa9u7okwvvg8CQJ117X8a2MElBGnmMd9tjLa/pXAx2I +bN7jSTSadXiPNYCx4ueiJa4Dwy+C8YkwUbhRf3+mc7Chnz0SXouTjh7OUeeA06jS +W2VAR2pKB0pdJtAkXxIy21Juu8KF5uZqVq1oimgKw2lRUIMdKaqsrVwESk6u5Ojj +3DS40q9DzFnwKGCuZpspvMdWYLscotzLrCbnHp+guWDigEHS3CKzKbNo327nVg6X +7UjqqtPZ2mCsnUx3QTDJsr3gcSqhzmB+Q6I/0Q2Nx/aMmbsNegu+LC3GjFtL59Bv +B8pB/MxID0j47SwPKQghZvb3 +-----END CERTIFICATE----- diff --git a/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.key b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.key new file mode 100644 index 0000000000000..8c278f843df24 --- /dev/null +++ b/src/Symfony/Component/HttpClient/Tests/Fixtures/tls/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuKnXkBSJwOwkKfR58wP/yLYW9QFX2THoqN8iffangRmZwc5K +LE6F1S8jYMv3JGiJ95Ij3MezAfuBCdgPqqP8JrR1XwjR1RFZMOL/4U9R9OuMVng0 +4PLwL6TzKoEtZuExHUWFP0+5AYblgno2hoN/HVuox8m6zQrBNcbhTgDIjP5Hn491 +d9odMtS3OxksDLr1UIOUGUWF7MQMN7lsN7rgT5qxoCkcAGAB4GPOA23HMt2zt4af +DiI7lAmuv8MKkTmBCcFe+q+U7o6wMxkjGstzAWRibtwzR4ejPwdO7se23MXCWGPv +F16Ztu1ip+e+waRus9o5UnyGaVPFAw8iCTC/KwIDAQABAoIBAQCczVNGe7oRADMh +EP/wM4ghhUTvHAndWrzFkFs4fJX1UKi34ZQoFTEdOZ6f1fHwj3f/qa8cDNJar5X9 +puJ+siotL3Suks2iT83dbhN63SCpiM2sqvuzu3Xp7vWwNOo5fqR2x46CmQ5uVn5S +EbZ09/mbEza5FvmwnB49rLepxY6F8P+vK5ZnCZYS2SHpOxv3U9wG8gmcHRI9ejbC +X9rwuu3oT23bfbJ0tn6Qh8O3R1kXZUUXqnxsn554cZZrXg5+ygbt4HfDVWMLpqy/ +5wG0FCpU8QvjF4L8qErP7TZRrWGFtti1RtACbu9LrWvO/74v54td5V28U6kqlDJR +ff4Mi4whAoGBAOBzReQIxGwoYApPyhF+ohvF39JEEXYfkzk94t6hbgyBFBFvqdFY +shT59im2P9LyDvTd5DnCIo52Sj7vM9H80tRjAA0A8okGOczk31ABbH8aZ2orU/0G +EJe4PV4r3bpLO6DKTYsicgRpXI3aHHLvYFXOVNrQKfrKCQ+GFMVuhDdRAoGBANKe +3Dn3XOq7EW42GZey1xUxrfQRJp491KXHvjYt7z7zSiUzqN+mqIqz6ngCjJWbyQsl +Ud9N9U+4rNfYYLHQ0resjxGQRtmooOHlLhT6pEplXDgQb2SmCg2u22SKkkXA7zOV +OFbNryXgkYThsA6ih8LiKM8aFn7zttRSEeTpfye7AoGBALhIzRyiuiuXpuswgdeF +YrJs8A1jB/c1i5qXHlvurT2lCYYbaZHSQj0I0r2CvrqDNhaEzStDIz5XDzTHD4Qd +EjmBo3wJyBkLPI/nZxb4ZE2jrz8znf0EasE3a2OTnrSjqqylDa/sMzM+EtkBORSB +SFaLV45lFeKs2W2eiBVmXTZRAoGAJoA7qaz6Iz6G9SqWixB6GLm4HsFz2cFbueJF +dwn2jf9TMnG7EQcaECDLX5y3rjGIEq2DxdouWaBcmChJpLeTjVfR31gMW4Vjw2dt +gRBAMAlPTkBS3Ictl0q7eCmMi4u1Liy828FFnxrp/uxyjnpPbuSAqTsPma1bYnyO +INY+FDkCgYAe9e39/vXe7Un3ysjqDUW+0OMM+kg4ulhiopzKY+QbHiSWmUUDtvcN +asqrYiX1d59e2ZNiqrlBn86I8549St81bWSrRMNf7R+WVb79RApsABeUaEoyo3lq +0UgOBM8Nt558kaja/YfJf/jwNC1DPuu5x5t38ZcqAkqrZ/HEPkFdGQ== +-----END RSA PRIVATE KEY----- diff --git a/src/Symfony/Component/HttpClient/TraceableHttpClient.php b/src/Symfony/Component/HttpClient/TraceableHttpClient.php index 4acbc8ee42df8..d60d0849cd95e 100644 --- a/src/Symfony/Component/HttpClient/TraceableHttpClient.php +++ b/src/Symfony/Component/HttpClient/TraceableHttpClient.php @@ -14,11 +14,12 @@ use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; use Symfony\Contracts\HttpClient\ResponseStreamInterface; +use Symfony\Contracts\Service\ResetInterface; /** * @author Jérémy Romey */ -final class TraceableHttpClient implements HttpClientInterface +final class TraceableHttpClient implements HttpClientInterface, ResetInterface { private $client; private $tracedRequests = []; @@ -68,6 +69,10 @@ public function getTracedRequests(): array public function reset() { + if ($this->client instanceof ResetInterface) { + $this->client->reset(); + } + $this->tracedRequests = []; } } diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json index dd5898dffe519..f282e562c0508 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -24,7 +24,8 @@ "php": "^7.2.5", "psr/log": "^1.0", "symfony/http-client-contracts": "^1.1.8|^2", - "symfony/polyfill-php73": "^1.11" + "symfony/polyfill-php73": "^1.11", + "symfony/service-contracts": "^1.0|^2" }, "require-dev": { "guzzlehttp/promises": "^1.3.1", @@ -33,12 +34,7 @@ "psr/http-client": "^1.0", "symfony/dependency-injection": "^4.4|^5.0", "symfony/http-kernel": "^4.4|^5.0", - "symfony/process": "^4.4|^5.0", - "symfony/service-contracts": "^1.0|^2", - "symfony/var-dumper": "^4.4|^5.0" - }, - "conflict": { - "symfony/http-kernel": "<4.4" + "symfony/process": "^4.4|^5.0" }, "autoload": { "psr-4": { "Symfony\\Component\\HttpClient\\": "" }, diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index ad839cefe4f6d..d37cbd29681c4 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -340,7 +340,7 @@ public static function trustXSendfileTypeHeader() } /** - * If this is set to true, the file will be unlinked after the request is send + * If this is set to true, the file will be unlinked after the request is sent * Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used. * * @return $this diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index bab37d1bee583..0af7d000af455 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -17,6 +17,9 @@ CHANGELOG * passing arguments to `Request::isMethodSafe()` is deprecated. * `ApacheRequest` is deprecated, use the `Request` class instead. * passing a third argument to `HeaderBag::get()` is deprecated, use method `all()` instead + * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column, + make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to + update your database. * `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column, make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database to speed up garbage collection of expired sessions. diff --git a/src/Symfony/Component/HttpFoundation/ServerBag.php b/src/Symfony/Component/HttpFoundation/ServerBag.php index 25da35ec595e0..02c70911c19f1 100644 --- a/src/Symfony/Component/HttpFoundation/ServerBag.php +++ b/src/Symfony/Component/HttpFoundation/ServerBag.php @@ -43,13 +43,13 @@ public function getHeaders() /* * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default * For this workaround to work, add these lines to your .htaccess file: - * RewriteCond %{HTTP:Authorization} ^(.+)$ - * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + * RewriteCond %{HTTP:Authorization} .+ + * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] * * A sample .htaccess file: * RewriteEngine On - * RewriteCond %{HTTP:Authorization} ^(.+)$ - * RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + * RewriteCond %{HTTP:Authorization} .+ + * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] * RewriteCond %{REQUEST_FILENAME} !-f * RewriteRule ^(.*)$ app.php [QSA,L] */ diff --git a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php index bb99c475b5798..37302128ad674 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php @@ -91,7 +91,10 @@ public function getName() return 'memory'; } - private function convertToBytes(string $memoryLimit): int + /** + * @return int|float + */ + private function convertToBytes(string $memoryLimit) { if ('-1' === $memoryLimit) { return -1; diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php index c1199f639edf8..b5e46106a7422 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ResettableServicePass.php @@ -43,16 +43,21 @@ public function process(ContainerBuilder $container) foreach ($container->findTaggedServiceIds($this->tagName, true) as $id => $tags) { $services[$id] = new Reference($id, ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE); - $attributes = $tags[0]; - if (!isset($attributes['method'])) { - throw new RuntimeException(sprintf('Tag %s requires the "method" attribute to be set.', $this->tagName)); - } + foreach ($tags as $attributes) { + if (!isset($attributes['method'])) { + throw new RuntimeException(sprintf('Tag "%s" requires the "method" attribute to be set.', $this->tagName)); + } + + if (!isset($methods[$id])) { + $methods[$id] = []; + } - $methods[$id] = $attributes['method']; + $methods[$id][] = $attributes['method']; + } } - if (empty($services)) { + if (!$services) { $container->removeAlias('services_resetter'); $container->removeDefinition('services_resetter'); diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php index 734fadbd74176..d9e0028ce1227 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/ServicesResetter.php @@ -35,7 +35,9 @@ public function __construct(\Traversable $resettableServices, array $resetMethod public function reset() { foreach ($this->resettableServices as $id => $service) { - $service->{$this->resetMethods[$id]}(); + foreach ((array) $this->resetMethods[$id] as $resetMethod) { + $service->$resetMethod(); + } } } } diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php index 4d855b724e349..26c361f754e53 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\EventListener; use Psr\Log\LoggerInterface; +use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -98,7 +99,7 @@ public function onControllerArguments(ControllerArgumentsEvent $event) $r = new \ReflectionFunction(\Closure::fromCallable($event->getController())); $r = $r->getParameters()[$k] ?? null; - if ($r && (!$r->hasType() || FlattenException::class === $r->getType()->getName())) { + if ($r && (!$r->hasType() || \in_array($r->getType()->getName(), [FlattenException::class, LegacyFlattenException::class], true))) { $arguments = $event->getArguments(); $arguments[$k] = FlattenException::createFromThrowable($e); $event->setArguments($arguments); diff --git a/src/Symfony/Component/HttpKernel/EventListener/LocaleAwareListener.php b/src/Symfony/Component/HttpKernel/EventListener/LocaleAwareListener.php index b93c4b0f03b46..62d03026a1c25 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/LocaleAwareListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/LocaleAwareListener.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 9475c47992e02..698af72ce2b2e 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -377,7 +377,9 @@ protected function validate(Request $request, Response $entry, bool $catch = fal } // add our cached last-modified validator - $subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified')); + if ($entry->headers->has('Last-Modified')) { + $subRequest->headers->set('if_modified_since', $entry->headers->get('Last-Modified')); + } // Add our cached etag validator to the environment. // We keep the etags from the client to handle the case when the client diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 226802fd562c4..b26b487072cbe 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -68,11 +68,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - const VERSION = '5.0.0'; - const VERSION_ID = 50000; + const VERSION = '5.0.1'; + const VERSION_ID = 50001; const MAJOR_VERSION = 5; const MINOR_VERSION = 0; - const RELEASE_VERSION = 0; + const RELEASE_VERSION = 1; const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '07/2020'; @@ -433,8 +433,9 @@ protected function initializeContainer() try { if (file_exists($cachePath) && \is_object($this->container = include $cachePath) - && (!$this->debug || (self::$freshCache[$k = $cachePath.'.'.$this->environment] ?? self::$freshCache[$k] = $cache->isFresh())) + && (!$this->debug || (self::$freshCache[$cachePath] ?? $cache->isFresh())) ) { + self::$freshCache[$cachePath] = true; $this->container->set('kernel', $this); error_reporting($errorLevel); @@ -458,7 +459,7 @@ protected function initializeContainer() $cache = new class($cachePath, $this->debug) extends ConfigCache { public $lock; - public function write($content, array $metadata = null) + public function write(string $content, array $metadata = null) { rewind($this->lock); ftruncate($this->lock, 0); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php index a857615f1c3d3..fadd820ea66c7 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; -use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ErrorController; diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php index d3c6869320910..9dbc2b08a4f26 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ResettableServicePassTest.php @@ -10,6 +10,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\ResettableServicePass; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService; +use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService; use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService; class ResettableServicePassTest extends TestCase @@ -23,6 +24,10 @@ public function testCompilerPass() $container->register('two', ClearableService::class) ->setPublic(true) ->addTag('kernel.reset', ['method' => 'clear']); + $container->register('three', MultiResettableService::class) + ->setPublic(true) + ->addTag('kernel.reset', ['method' => 'resetFirst']) + ->addTag('kernel.reset', ['method' => 'resetSecond']); $container->register('services_resetter', ServicesResetter::class) ->setPublic(true) @@ -38,10 +43,12 @@ public function testCompilerPass() new IteratorArgument([ 'one' => new Reference('one', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE), 'two' => new Reference('two', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE), + 'three' => new Reference('three', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE), ]), [ - 'one' => 'reset', - 'two' => 'clear', + 'one' => ['reset'], + 'two' => ['clear'], + 'three' => ['resetFirst', 'resetSecond'], ], ], $definition->getArguments() @@ -51,7 +58,7 @@ public function testCompilerPass() public function testMissingMethod() { $this->expectException('Symfony\Component\DependencyInjection\Exception\RuntimeException'); - $this->expectExceptionMessage('Tag kernel.reset requires the "method" attribute to be set.'); + $this->expectExceptionMessage('Tag "kernel.reset" requires the "method" attribute to be set.'); $container = new ContainerBuilder(); $container->register(ResettableService::class) ->addTag('kernel.reset'); diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php index 5be6026c90a67..604d2b0d13b82 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/ServicesResetterTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\HttpKernel\Tests\Fixtures\ClearableService; +use Symfony\Component\HttpKernel\Tests\Fixtures\MultiResettableService; use Symfony\Component\HttpKernel\Tests\Fixtures\ResettableService; class ServicesResetterTest extends TestCase @@ -22,6 +23,8 @@ protected function setUp(): void { ResettableService::$counter = 0; ClearableService::$counter = 0; + MultiResettableService::$resetFirstCounter = 0; + MultiResettableService::$resetSecondCounter = 0; } public function testResetServices() @@ -29,14 +32,18 @@ public function testResetServices() $resetter = new ServicesResetter(new \ArrayIterator([ 'id1' => new ResettableService(), 'id2' => new ClearableService(), + 'id3' => new MultiResettableService(), ]), [ - 'id1' => 'reset', - 'id2' => 'clear', + 'id1' => ['reset'], + 'id2' => ['clear'], + 'id3' => ['resetFirst', 'resetSecond'], ]); $resetter->reset(); - $this->assertEquals(1, ResettableService::$counter); - $this->assertEquals(1, ClearableService::$counter); + $this->assertSame(1, ResettableService::$counter); + $this->assertSame(1, ClearableService::$counter); + $this->assertSame(1, MultiResettableService::$resetFirstCounter); + $this->assertSame(1, MultiResettableService::$resetSecondCounter); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/MultiResettableService.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/MultiResettableService.php new file mode 100644 index 0000000000000..4930fd6a30c19 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/MultiResettableService.php @@ -0,0 +1,19 @@ +setNextResponse(200, [], 'Hello World', function ($request, $response) { + $this->assertFalse($request->headers->has('If-Modified-Since')); $response->headers->set('Cache-Control', 'public'); $response->headers->set('ETag', '"12345"'); if ($response->getETag() == $request->headers->get('IF_NONE_MATCH')) { diff --git a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php index e185c6f862e53..e9f100a8afa8f 100644 --- a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php @@ -29,7 +29,7 @@ class KernelTest extends TestCase { - public static function tearDownAfterClass(): void + protected function tearDown(): void { $fs = new Filesystem(); $fs->remove(__DIR__.'/Fixtures/var'); diff --git a/src/Symfony/Component/Intl/Currencies.php b/src/Symfony/Component/Intl/Currencies.php index 7459d633cba9e..c155c8f09f425 100644 --- a/src/Symfony/Component/Intl/Currencies.php +++ b/src/Symfony/Component/Intl/Currencies.php @@ -46,7 +46,7 @@ public static function exists(string $currency): bool } /** - * @throws MissingResourceException if the currency code does not exists + * @throws MissingResourceException if the currency code does not exist */ public static function getName(string $currency, string $displayLocale = null): string { @@ -78,7 +78,7 @@ public static function getNames(string $displayLocale = null): array } /** - * @throws MissingResourceException if the currency code does not exists + * @throws MissingResourceException if the currency code does not exist */ public static function getSymbol(string $currency, string $displayLocale = null): string { @@ -115,7 +115,7 @@ public static function getNumericCode(string $currency): int } /** - * @throws MissingResourceException if the numeric code does not exists + * @throws MissingResourceException if the numeric code does not exist */ public static function forNumericCode(int $numericCode): array { diff --git a/src/Symfony/Component/Intl/Locales.php b/src/Symfony/Component/Intl/Locales.php index aee16beb16678..1b2d0d2adf258 100644 --- a/src/Symfony/Component/Intl/Locales.php +++ b/src/Symfony/Component/Intl/Locales.php @@ -49,7 +49,7 @@ public static function exists(string $locale): bool } /** - * @throws MissingResourceException if the locale does not exists + * @throws MissingResourceException if the locale does not exist */ public static function getName(string $locale, string $displayLocale = null): string { diff --git a/src/Symfony/Component/Intl/Scripts.php b/src/Symfony/Component/Intl/Scripts.php index 943ef8b46919f..bb29f0095bee3 100644 --- a/src/Symfony/Component/Intl/Scripts.php +++ b/src/Symfony/Component/Intl/Scripts.php @@ -41,7 +41,7 @@ public static function exists(string $script): bool } /** - * @throws MissingResourceException if the script code does not exists + * @throws MissingResourceException if the script code does not exist */ public static function getName(string $script, string $displayLocale = null): string { diff --git a/src/Symfony/Component/Ldap/Ldap.php b/src/Symfony/Component/Ldap/Ldap.php index 24f66ac7e1a9c..32c2e853af604 100644 --- a/src/Symfony/Component/Ldap/Ldap.php +++ b/src/Symfony/Component/Ldap/Ldap.php @@ -75,11 +75,7 @@ public function escape(string $subject, string $ignore = '', int $flags = 0): st public static function create(string $adapter, array $config = []): self { if (!isset(self::$adapterMap[$adapter])) { - throw new DriverNotFoundException(sprintf( - 'Adapter "%s" not found. You should use one of: %s', - $adapter, - implode(', ', self::$adapterMap) - )); + throw new DriverNotFoundException(sprintf('Adapter "%s" not found. You should use one of: %s', $adapter, implode(', ', self::$adapterMap))); } $class = self::$adapterMap[$adapter]; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php index 2c90472fc6dc6..17f7a7fcf3364 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillHttpTransport.php @@ -60,7 +60,7 @@ protected function doSendHttp(SentMessage $message): ResponseInterface throw new HttpTransportException(sprintf('Unable to send an email (code %s).', $result['code']), $response); } - $message->setMessageId($result['_id']); + $message->setMessageId($result[0]['_id']); return $response; } diff --git a/src/Symfony/Component/Mailer/Transport/AbstractTransport.php b/src/Symfony/Component/Mailer/Transport/AbstractTransport.php index f8dd7d4c574ef..1bc3fa12a5616 100644 --- a/src/Symfony/Component/Mailer/Transport/AbstractTransport.php +++ b/src/Symfony/Component/Mailer/Transport/AbstractTransport.php @@ -13,8 +13,8 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use Symfony\Component\Mailer\Envelope; use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; +use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mime\Address; diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php index 1a40e0c5327a0..e6d107eeaf1d1 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Messenger\Stamp\SentToFailureTransportStamp; use Symfony\Component\Messenger\Stamp\TransportMessageIdStamp; use Symfony\Component\Messenger\Transport\Receiver\ListableReceiverInterface; +use Symfony\Component\Messenger\Transport\Receiver\ReceiverInterface; /** * @group time-sensitive @@ -94,4 +95,101 @@ public function testMultipleRedeliveryFails() $redeliveryStamp2->getRedeliveredAt()->format('Y-m-d H:i:s')), $tester->getDisplay(true)); } + + public function testReceiverShouldBeListable() + { + $receiver = $this->createMock(ReceiverInterface::class); + $command = new FailedMessagesShowCommand( + 'failure_receiver', + $receiver + ); + + $this->expectExceptionMessage('The "failure_receiver" receiver does not support listing or showing specific messages.'); + + $tester = new CommandTester($command); + $tester->execute(['id' => 15]); + } + + public function testListMessages() + { + $sentToFailureStamp = new SentToFailureTransportStamp('async'); + $redeliveryStamp = new RedeliveryStamp(0, 'Things are bad!'); + $envelope = new Envelope(new \stdClass(), [ + new TransportMessageIdStamp(15), + $sentToFailureStamp, + $redeliveryStamp, + ]); + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]); + + $command = new FailedMessagesShowCommand( + 'failure_receiver', + $receiver + ); + + $tester = new CommandTester($command); + $tester->execute([]); + $this->assertStringContainsString(sprintf(<<getRedeliveredAt()->format('Y-m-d H:i:s')), + $tester->getDisplay(true)); + } + + public function testListMessagesReturnsNoMessagesFound() + { + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('all')->with()->willReturn([]); + + $command = new FailedMessagesShowCommand( + 'failure_receiver', + $receiver + ); + + $tester = new CommandTester($command); + $tester->execute([]); + $this->assertStringContainsString('[OK] No failed messages were found.', $tester->getDisplay(true)); + } + + public function testListMessagesReturnsPaginatedMessages() + { + $sentToFailureStamp = new SentToFailureTransportStamp('async'); + $envelope = new Envelope(new \stdClass(), [ + new TransportMessageIdStamp(15), + $sentToFailureStamp, + new RedeliveryStamp(0, 'Things are bad!'), + ]); + $receiver = $this->createMock(ListableReceiverInterface::class); + $receiver->expects($this->once())->method('all')->with()->willReturn([$envelope]); + + $command = new FailedMessagesShowCommand( + 'failure_receiver', + $receiver + ); + + $tester = new CommandTester($command); + $tester->execute(['--max' => 1]); + $this->assertStringContainsString('Showing first 1 messages.', $tester->getDisplay(true)); + } + + public function testInvalidMessagesThrowsException() + { + $sentToFailureStamp = new SentToFailureTransportStamp('async'); + $envelope = new Envelope(new \stdClass(), [ + new TransportMessageIdStamp(15), + $sentToFailureStamp, + ]); + $receiver = $this->createMock(ListableReceiverInterface::class); + + $command = new FailedMessagesShowCommand( + 'failure_receiver', + $receiver + ); + + $this->expectExceptionMessage('The message "15" was not found.'); + + $tester = new CommandTester($command); + $tester->execute(['id' => 15]); + } } diff --git a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php index 60d5e806e357c..b3cb7a6dc8261 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/AmqpExt/AmqpTransportFactoryTest.php @@ -28,6 +28,9 @@ public function testSupportsOnlyAmqpTransports() $this->assertFalse($factory->supports('invalid-dsn', [])); } + /** + * @requires extension amqp + */ public function testItCreatesTheTransport() { $factory = new AmqpTransportFactory(); diff --git a/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php b/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php index 22149f8a391aa..6fddc3fbbc3e5 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/InMemoryTransportTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Tests\Fixtures\AnEnvelopeStamp; use Symfony\Component\Messenger\Transport\InMemoryTransport; /** @@ -50,6 +51,19 @@ public function testQueue() $this->assertSame([], $this->transport->get()); } + public function testAcknowledgeSameMessageWithDifferentStamps() + { + $envelope1 = new Envelope(new \stdClass(), [new AnEnvelopeStamp()]); + $this->transport->send($envelope1); + $envelope2 = new Envelope(new \stdClass(), [new AnEnvelopeStamp()]); + $this->transport->send($envelope2); + $this->assertSame([$envelope1, $envelope2], $this->transport->get()); + $this->transport->ack($envelope1->with(new AnEnvelopeStamp())); + $this->assertSame([$envelope2], $this->transport->get()); + $this->transport->reject($envelope2->with(new AnEnvelopeStamp())); + $this->assertSame([], $this->transport->get()); + } + public function testAck() { $envelope = new Envelope(new \stdClass()); diff --git a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php index 8e37e3873f486..6d4db85b0b259 100644 --- a/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/AmqpExt/Connection.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Transport\AmqpExt; use Symfony\Component\Messenger\Exception\InvalidArgumentException; +use Symfony\Component\Messenger\Exception\LogicException; /** * An AMQP connection. @@ -58,6 +59,10 @@ class Connection public function __construct(array $connectionOptions, array $exchangeOptions, array $queuesOptions, AmqpFactory $amqpFactory = null) { + if (!\extension_loaded('amqp')) { + throw new LogicException(sprintf('You cannot use the "%s" as the "amqp" extension is not installed.', __CLASS__)); + } + $this->connectionOptions = array_replace_recursive([ 'delay' => [ 'exchange_name' => 'delays', diff --git a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php index 4541a1095db43..50e95ef5e2893 100644 --- a/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php +++ b/src/Symfony/Component/Messenger/Transport/Doctrine/DoctrineTransportFactory.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Messenger\Transport\Doctrine; use Doctrine\Common\Persistence\ConnectionRegistry; +use Symfony\Bridge\Doctrine\RegistryInterface; use Symfony\Component\Messenger\Exception\TransportException; use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface; use Symfony\Component\Messenger\Transport\TransportFactoryInterface; @@ -24,8 +25,12 @@ class DoctrineTransportFactory implements TransportFactoryInterface { private $registry; - public function __construct(ConnectionRegistry $registry) + public function __construct($registry) { + if (!$registry instanceof RegistryInterface && !$registry instanceof ConnectionRegistry) { + throw new \TypeError(sprintf('Expected an instance of %s or %s, but got %s.', RegistryInterface::class, ConnectionRegistry::class, \is_object($registry) ? \get_class($registry) : \gettype($registry))); + } + $this->registry = $registry; } diff --git a/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php b/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php index 354bb601a140a..09cbb31a041fd 100644 --- a/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php +++ b/src/Symfony/Component/Messenger/Transport/InMemoryTransport.php @@ -55,7 +55,7 @@ public function get(): iterable public function ack(Envelope $envelope): void { $this->acknowledged[] = $envelope; - $id = spl_object_hash($envelope); + $id = spl_object_hash($envelope->getMessage()); unset($this->queue[$id]); } @@ -65,7 +65,7 @@ public function ack(Envelope $envelope): void public function reject(Envelope $envelope): void { $this->rejected[] = $envelope; - $id = spl_object_hash($envelope); + $id = spl_object_hash($envelope->getMessage()); unset($this->queue[$id]); } @@ -75,7 +75,7 @@ public function reject(Envelope $envelope): void public function send(Envelope $envelope): Envelope { $this->sent[] = $envelope; - $id = spl_object_hash($envelope); + $id = spl_object_hash($envelope->getMessage()); $this->queue[$id] = $envelope; return $envelope; diff --git a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php index 88aa1a316a786..6838620325668 100644 --- a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php +++ b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php @@ -56,11 +56,20 @@ public function getParts(): array private function prepareFields(array $fields): array { $values = []; - array_walk_recursive($fields, function ($item, $key) use (&$values) { - if (!\is_array($item)) { - $values[] = $this->preparePart($key, $item); + + $prepare = function ($item, $key, $root = null) use (&$values, &$prepare) { + $fieldName = $root ? sprintf('%s[%s]', $root, $key) : $key; + + if (\is_array($item)) { + array_walk($item, $prepare, $fieldName); + + return; } - }); + + $values[] = $this->preparePart($fieldName, $item); + }; + + array_walk($fields, $prepare); return $values; } diff --git a/src/Symfony/Component/Mime/Tests/Part/Multipart/FormDataPartTest.php b/src/Symfony/Component/Mime/Tests/Part/Multipart/FormDataPartTest.php index 71a03e6863d8a..a74ecea316ec9 100644 --- a/src/Symfony/Component/Mime/Tests/Part/Multipart/FormDataPartTest.php +++ b/src/Symfony/Component/Mime/Tests/Part/Multipart/FormDataPartTest.php @@ -47,6 +47,34 @@ public function testConstructor() $this->assertEquals([$t, $b, $c], $f->getParts()); } + public function testNestedArrayParts() + { + $p1 = new TextPart('content', 'utf-8', 'plain', '8bit'); + $f = new FormDataPart([ + 'foo' => clone $p1, + 'bar' => [ + 'baz' => [ + clone $p1, + 'qux' => clone $p1, + ], + ], + ]); + + $this->assertEquals('multipart', $f->getMediaType()); + $this->assertEquals('form-data', $f->getMediaSubtype()); + + $p1->setName('foo'); + $p1->setDisposition('form-data'); + + $p2 = clone $p1; + $p2->setName('bar[baz][0]'); + + $p3 = clone $p1; + $p3->setName('bar[baz][qux]'); + + $this->assertEquals([$p1, $p2, $p3], $f->getParts()); + } + public function testToString() { $p = DataPart::fromPath($file = __DIR__.'/../../Fixtures/mimetypes/test.gif'); diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportFactoryTest.php new file mode 100644 index 0000000000000..afd77200ba8e8 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportFactoryTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Slack\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory; +use Symfony\Component\Notifier\Exception\IncompleteDsnException; +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\Dsn; + +final class SlackTransportFactoryTest extends TestCase +{ + public function testCreateWithDsn(): void + { + $factory = new SlackTransportFactory(); + + $host = 'testHost'; + $channel = 'testChannel'; + $transport = $factory->create(Dsn::fromString(sprintf('slack://testUser@%s/?channel=%s', $host, $channel))); + + $this->assertSame(sprintf('slack://%s?channel=%s', $host, $channel), (string) $transport); + } + + public function testCreateWithNoTokenThrowsMalformed(): void + { + $factory = new SlackTransportFactory(); + + $this->expectException(IncompleteDsnException::class); + $factory->create(Dsn::fromString(sprintf('slack://%s/?channel=%s', 'testHost', 'testChannel'))); + } + + public function testSupportsSlackScheme(): void + { + $factory = new SlackTransportFactory(); + + $this->assertTrue($factory->supports(Dsn::fromString('slack://host/?channel=testChannel'))); + $this->assertFalse($factory->supports(Dsn::fromString('somethingElse://host/?channel=testChannel'))); + } + + public function testNonSlackSchemeThrows(): void + { + $factory = new SlackTransportFactory(); + + $this->expectException(UnsupportedSchemeException::class); + + $factory->create(Dsn::fromString('somethingElse://user:pwd@host/?channel=testChannel')); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php new file mode 100644 index 0000000000000..cbfaadd6060c1 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Slack/Tests/SlackTransportTest.php @@ -0,0 +1,212 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Slack\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Slack\SlackOptions; +use Symfony\Component\Notifier\Bridge\Slack\SlackTransport; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\MessageOptionsInterface; +use Symfony\Component\Notifier\Notification\Notification; +use Symfony\Component\Notifier\Recipient\Recipient; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class SlackTransportTest extends TestCase +{ + public function testToStringContainsProperties(): void + { + $host = 'testHost'; + $channel = 'testChannel'; + + $transport = new SlackTransport('testToken', $channel, $this->createMock(HttpClientInterface::class)); + $transport->setHost('testHost'); + + $this->assertSame(sprintf('slack://%s?channel=%s', $host, $channel), (string) $transport); + } + + public function testSupportsChatMessage(): void + { + $transport = new SlackTransport('testToken', 'testChannel', $this->createMock(HttpClientInterface::class)); + + $this->assertTrue($transport->supports(new ChatMessage('testChatMessage'))); + $this->assertFalse($transport->supports($this->createMock(MessageInterface::class))); + } + + public function testSendNonChatMessageThrows(): void + { + $this->expectException(LogicException::class); + + $transport = new SlackTransport('testToken', 'testChannel', $this->createMock(HttpClientInterface::class)); + + $transport->send($this->createMock(MessageInterface::class)); + } + + public function testSendWithEmptyArrayResponseThrows(): void + { + $this->expectException(TransportException::class); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(500); + $response->expects($this->once()) + ->method('getContent') + ->willReturn('[]'); + + $client = new MockHttpClient(static function () use ($response): ResponseInterface { + return $response; + }); + + $transport = new SlackTransport('testToken', 'testChannel', $client); + + $transport->send(new ChatMessage('testMessage')); + } + + public function testSendWithErrorResponseThrows(): void + { + $this->expectException(TransportException::class); + $this->expectExceptionMessageRegExp('/testErrorCode/'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(400); + + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['error' => 'testErrorCode'])); + + $client = new MockHttpClient(static function () use ($response): ResponseInterface { + return $response; + }); + + $transport = new SlackTransport('testToken', 'testChannel', $client); + + $transport->send(new ChatMessage('testMessage')); + } + + public function testSendWithOptions(): void + { + $token = 'testToken'; + $channel = 'testChannel'; + $message = 'testMessage'; + + $response = $this->createMock(ResponseInterface::class); + + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['ok' => true])); + + $expectedBody = sprintf('token=%s&channel=%s&text=%s', $token, $channel, $message); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertSame($expectedBody, $options['body']); + + return $response; + }); + + $transport = new SlackTransport($token, $channel, $client); + + $transport->send(new ChatMessage('testMessage')); + } + + public function testSendWithNotification(): void + { + $token = 'testToken'; + $channel = 'testChannel'; + $message = 'testMessage'; + + $response = $this->createMock(ResponseInterface::class); + + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['ok' => true])); + + $notification = new Notification($message); + $chatMessage = ChatMessage::fromNotification($notification, new Recipient('test-email@example.com')); + $options = SlackOptions::fromNotification($notification); + + $expectedBody = http_build_query([ + 'blocks' => $options->toArray()['blocks'], + 'token' => $token, + 'channel' => $channel, + 'text' => $message, + ]); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertSame($expectedBody, $options['body']); + + return $response; + }); + + $transport = new SlackTransport($token, $channel, $client); + + $transport->send($chatMessage); + } + + public function testSendWithInvalidOptions(): void + { + $this->expectException(LogicException::class); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []): ResponseInterface { + return $this->createMock(ResponseInterface::class); + }); + + $transport = new SlackTransport('testToken', 'testChannel', $client); + + $transport->send(new ChatMessage('testMessage', $this->createMock(MessageOptionsInterface::class))); + } + + public function testSendWith200ResponseButNotOk(): void + { + $token = 'testToken'; + $channel = 'testChannel'; + $message = 'testMessage'; + + $this->expectException(TransportException::class); + + $response = $this->createMock(ResponseInterface::class); + + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['ok' => false, 'error' => 'testErrorCode'])); + + $expectedBody = sprintf('token=%s&channel=%s&text=%s', $token, $channel, $message); + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertSame($expectedBody, $options['body']); + + return $response; + }); + + $transport = new SlackTransport($token, $channel, $client); + + $transport->send(new ChatMessage('testMessage')); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/composer.json b/src/Symfony/Component/Notifier/Bridge/Slack/composer.json index feb1e5f4edbb9..9b9d08930a9be 100644 --- a/src/Symfony/Component/Notifier/Bridge/Slack/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Slack/composer.json @@ -20,6 +20,9 @@ "symfony/http-client": "^4.3|^5.0", "symfony/notifier": "~5.0.0" }, + "require-dev": { + "symfony/event-dispatcher": "^4.3|^5.0" + }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Slack\\": "" }, "exclude-from-classmap": [ diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportFactoryTest.php b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportFactoryTest.php new file mode 100644 index 0000000000000..c74d38f2f6882 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportFactoryTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Telegram\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory; +use Symfony\Component\Notifier\Exception\IncompleteDsnException; +use Symfony\Component\Notifier\Exception\UnsupportedSchemeException; +use Symfony\Component\Notifier\Transport\Dsn; + +final class TelegramTransportFactoryTest extends TestCase +{ + public function testCreateWithDsn(): void + { + $factory = new TelegramTransportFactory(); + + $host = 'testHost'; + $channel = 'testChannel'; + + $transport = $factory->create(Dsn::fromString(sprintf('telegram://%s@%s/?channel=%s', 'testUser:testPassword', $host, $channel))); + + $this->assertSame(sprintf('telegram://%s?channel=%s', $host, $channel), (string) $transport); + } + + public function testCreateWithNoPasswordThrowsMalformed(): void + { + $factory = new TelegramTransportFactory(); + + $this->expectException(IncompleteDsnException::class); + $factory->create(Dsn::fromString(sprintf('telegram://%s@%s/?channel=%s', 'simpleToken', 'testHost', 'testChannel'))); + } + + public function testCreateWithNoTokenThrowsMalformed(): void + { + $factory = new TelegramTransportFactory(); + + $this->expectException(IncompleteDsnException::class); + $factory->create(Dsn::fromString(sprintf('telegram://%s/?channel=%s', 'testHost', 'testChannel'))); + } + + public function testSupportsTelegramScheme(): void + { + $factory = new TelegramTransportFactory(); + + $this->assertTrue($factory->supports(Dsn::fromString('telegram://host/?channel=testChannel'))); + $this->assertFalse($factory->supports(Dsn::fromString('somethingElse://host/?channel=testChannel'))); + } + + public function testNonTelegramSchemeThrows(): void + { + $factory = new TelegramTransportFactory(); + + $this->expectException(UnsupportedSchemeException::class); + $factory->create(Dsn::fromString('somethingElse://user:pwd@host/?channel=testChannel')); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php new file mode 100644 index 0000000000000..352b390ffd00b --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Telegram/Tests/TelegramTransportTest.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Telegram\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransport; +use Symfony\Component\Notifier\Exception\LogicException; +use Symfony\Component\Notifier\Exception\TransportException; +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Message\MessageInterface; +use Symfony\Component\Notifier\Message\MessageOptionsInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +final class TelegramTransportTest extends TestCase +{ + public function testToStringContainsProperties(): void + { + $channel = 'testChannel'; + + $transport = new TelegramTransport('testToken', $channel, $this->createMock(HttpClientInterface::class)); + $transport->setHost('testHost'); + + $this->assertSame(sprintf('telegram://%s?channel=%s', 'testHost', $channel), (string) $transport); + } + + public function testSupportsChatMessage(): void + { + $transport = new TelegramTransport('testToken', 'testChannel', $this->createMock(HttpClientInterface::class)); + + $this->assertTrue($transport->supports(new ChatMessage('testChatMessage'))); + $this->assertFalse($transport->supports($this->createMock(MessageInterface::class))); + } + + public function testSendNonChatMessageThrows(): void + { + $this->expectException(LogicException::class); + $transport = new TelegramTransport('testToken', 'testChannel', $this->createMock(HttpClientInterface::class)); + + $transport->send($this->createMock(MessageInterface::class)); + } + + public function testSendWithErrorResponseThrows(): void + { + $this->expectException(TransportException::class); + $this->expectExceptionMessageRegExp('/testDescription.+testErrorCode/'); + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(400); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(json_encode(['description' => 'testDescription', 'error_code' => 'testErrorCode'])); + + $client = new MockHttpClient(static function () use ($response): ResponseInterface { + return $response; + }); + + $transport = new TelegramTransport('testToken', 'testChannel', $client); + + $transport->send(new ChatMessage('testMessage')); + } + + public function testSendWithOptions(): void + { + $channel = 'testChannel'; + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(''); + + $expectedBody = [ + 'chat_id' => $channel, + 'text' => 'testMessage', + 'parse_mode' => 'Markdown', + ]; + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertEquals($expectedBody, json_decode($options['body'], true)); + + return $response; + }); + + $transport = new TelegramTransport('testToken', $channel, $client); + + $transport->send(new ChatMessage('testMessage')); + } + + public function testSendWithChannelOverride(): void + { + $channelOverride = 'channelOverride'; + + $response = $this->createMock(ResponseInterface::class); + $response->expects($this->exactly(2)) + ->method('getStatusCode') + ->willReturn(200); + $response->expects($this->once()) + ->method('getContent') + ->willReturn(''); + + $expectedBody = [ + 'chat_id' => $channelOverride, + 'text' => 'testMessage', + 'parse_mode' => 'Markdown', + ]; + + $client = new MockHttpClient(function (string $method, string $url, array $options = []) use ($response, $expectedBody): ResponseInterface { + $this->assertEquals($expectedBody, json_decode($options['body'], true)); + + return $response; + }); + + $transport = new TelegramTransport('testToken', 'defaultChannel', $client); + + $messageOptions = $this->createMock(MessageOptionsInterface::class); + $messageOptions + ->expects($this->once()) + ->method('getRecipientId') + ->willReturn($channelOverride); + + $transport->send(new ChatMessage('testMessage', $messageOptions)); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json b/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json index a562f74dce549..003215dd49e9a 100644 --- a/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Telegram/composer.json @@ -20,6 +20,9 @@ "symfony/http-client": "^4.3|^5.0", "symfony/notifier": "~5.0.0" }, + "require-dev": { + "symfony/event-dispatcher": "^4.3|^5.0" + }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Telegram\\": "" }, "exclude-from-classmap": [ diff --git a/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php b/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php index 338bb4e0c6eb9..6c3decc53cfda 100644 --- a/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php +++ b/src/Symfony/Component/Process/Tests/PhpExecutableFinderTest.php @@ -46,4 +46,16 @@ public function testFindArguments() $this->assertEquals($f->findArguments(), [], '::findArguments() returns no arguments'); } } + + public function testNotExitsBinaryFile() + { + $f = new PhpExecutableFinder(); + $phpBinaryEnv = PHP_BINARY; + putenv('PHP_BINARY=/usr/local/php/bin/php-invalid'); + + $this->assertFalse($f->find(), '::find() returns false because of not exist file'); + $this->assertFalse($f->find(false), '::find(false) returns false because of not exist file'); + + putenv('PHP_BINARY='.$phpBinaryEnv); + } } diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index 3af07c7cf79f1..47eace5b0d817 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -188,13 +188,14 @@ private static function throwInvalidArgumentException(string $message, array $tr return; } - if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file'] && \array_key_exists(0, $trace[$i]['args'])) { + if (isset($trace[$i]['file']) && __FILE__ === $trace[$i]['file']) { $pos = strpos($message, $delim = 'must be of the type ') ?: (strpos($message, $delim = 'must be an instance of ') ?: strpos($message, $delim = 'must implement interface ')); $pos += \strlen($delim); - $type = $trace[$i]['args'][0]; - $type = \is_object($type) ? \get_class($type) : \gettype($type); + $j = strpos($message, ',', $pos); + $type = substr($message, 2 + $j, strpos($message, ' given', $j) - $j - 2); + $message = substr($message, $pos, $j - $pos); - throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', substr($message, $pos, strpos($message, ',', $pos) - $pos), $type, $propertyPath)); + throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $message, 'NULL' === $type ? 'null' : $type, $propertyPath)); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 1b0e47b1063c5..218f18730f162 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -546,7 +546,7 @@ public function testThrowTypeError() public function testThrowTypeErrorWithNullArgument() { $this->expectException('Symfony\Component\PropertyAccess\Exception\InvalidArgumentException'); - $this->expectExceptionMessage('Expected argument of type "DateTime", "NULL" given'); + $this->expectExceptionMessage('Expected argument of type "DateTime", "null" given'); $object = new TypeHinted(); $this->propertyAccessor->setValue($object, 'date', null); diff --git a/src/Symfony/Component/Routing/Loader/ObjectLoader.php b/src/Symfony/Component/Routing/Loader/ObjectLoader.php index aefb8295115ab..1a375d95ff013 100644 --- a/src/Symfony/Component/Routing/Loader/ObjectLoader.php +++ b/src/Symfony/Component/Routing/Loader/ObjectLoader.php @@ -42,10 +42,15 @@ abstract protected function getObject(string $id); */ public function load($resource, string $type = null) { - if (!preg_match('/^[^\:]+(?:::(?:[^\:]+))?$/', $resource)) { + if (!preg_match('/^[^\:]+(?:::?(?:[^\:]+))?$/', $resource)) { throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the %s route loader: use the format "object_id::method" or "object_id" if your object class has an "__invoke" method.', $resource, \is_string($type) ? '"'.$type.'"' : 'object')); } + if (1 === substr_count($resource, ':')) { + $resource = str_replace(':', '::', $resource); + @trigger_error(sprintf('Referencing object route loaders with a single colon is deprecated since Symfony 4.1. Use %s instead.', $resource), E_USER_DEPRECATED); + } + $parts = explode('::', $resource); $method = $parts[1] ?? '__invoke'; diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index 81f8b04dbd44f..4c76231e3de81 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -93,9 +93,7 @@ public function match(string $pathinfo) throw new NoConfigurationException(); } - throw 0 < \count($this->allow) - ? new MethodNotAllowedException(array_unique($this->allow)) - : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo)); + throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo)); } /** diff --git a/src/Symfony/Component/Routing/RequestContext.php b/src/Symfony/Component/Routing/RequestContext.php index 6aa88ffed9b73..cb655f4485057 100644 --- a/src/Symfony/Component/Routing/RequestContext.php +++ b/src/Symfony/Component/Routing/RequestContext.php @@ -57,8 +57,8 @@ public function fromRequest(Request $request) $this->setMethod($request->getMethod()); $this->setHost($request->getHost()); $this->setScheme($request->getScheme()); - $this->setHttpPort($request->isSecure() ? $this->httpPort : $request->getPort()); - $this->setHttpsPort($request->isSecure() ? $request->getPort() : $this->httpsPort); + $this->setHttpPort($request->isSecure() || null === $request->getPort() ? $this->httpPort : $request->getPort()); + $this->setHttpsPort($request->isSecure() && null !== $request->getPort() ? $request->getPort() : $this->httpsPort); $this->setQueryString($request->server->get('QUERY_STRING', '')); return $this; diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index d5c28dc67ab88..7d971ee50440a 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -15,7 +15,7 @@ CHANGELOG **After** ```php - if ($this->authorizationChecker->isGranted(new Expression("has_role('ROLE_USER') or has_role('ROLE_ADMIN')"))) {} + if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {} // or: if ($this->authorizationChecker->isGranted('ROLE_USER') || $this->authorizationChecker->isGranted('ROLE_ADMIN') @@ -62,6 +62,7 @@ CHANGELOG * Deprecated returning a non-boolean value when implementing `Guard\AuthenticatorInterface::checkCredentials()`. * Deprecated passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()` * Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.) + * Added `AbstractListener` which replaces the deprecated `ListenerInterface` 4.3.0 ----- diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index 3f62de4ef8e6a..b9c984f21d334 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -21,6 +21,7 @@ use Symfony\Component\Security\Guard\AuthenticatorInterface; use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; use Symfony\Component\Security\Guard\Token\PreAuthenticationGuardToken; +use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; /** @@ -31,7 +32,7 @@ * * @final */ -class GuardAuthenticationListener +class GuardAuthenticationListener extends AbstractListener { private $guardHandler; private $authenticationManager; @@ -58,9 +59,9 @@ public function __construct(GuardAuthenticatorHandler $guardHandler, Authenticat } /** - * Iterates over each authenticator to see if each wants to authenticate the request. + * {@inheritdoc} */ - public function __invoke(RequestEvent $event) + public function supports(Request $request): ?bool { if (null !== $this->logger) { $context = ['firewall_key' => $this->providerKey]; @@ -72,7 +73,39 @@ public function __invoke(RequestEvent $event) $this->logger->debug('Checking for guard authentication credentials.', $context); } + $guardAuthenticators = []; + foreach ($this->guardAuthenticators as $key => $guardAuthenticator) { + if (null !== $this->logger) { + $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); + } + + if ($guardAuthenticator->supports($request)) { + $guardAuthenticators[$key] = $guardAuthenticator; + } elseif (null !== $this->logger) { + $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); + } + } + + if (!$guardAuthenticators) { + return false; + } + + $request->attributes->set('_guard_authenticators', $guardAuthenticators); + + return true; + } + + /** + * Iterates over each authenticator to see if each wants to authenticate the request. + */ + public function authenticate(RequestEvent $event) + { + $request = $event->getRequest(); + $guardAuthenticators = $request->attributes->get('_guard_authenticators'); + $request->attributes->remove('_guard_authenticators'); + + foreach ($guardAuthenticators as $key => $guardAuthenticator) { // get a key that's unique to *this* guard authenticator // this MUST be the same as GuardAuthenticationProvider $uniqueGuardKey = $this->providerKey.'_'.$key; @@ -93,19 +126,6 @@ private function executeGuardAuthenticator(string $uniqueGuardKey, Authenticator { $request = $event->getRequest(); try { - if (null !== $this->logger) { - $this->logger->debug('Checking support on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); - } - - // abort the execution of the authenticator if it doesn't support the request - if (!$guardAuthenticator->supports($request)) { - if (null !== $this->logger) { - $this->logger->debug('Guard authenticator does not support the request.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); - } - - return; - } - if (null !== $this->logger) { $this->logger->debug('Calling getCredentials() on guard authenticator.', ['firewall_key' => $this->providerKey, 'authenticator' => \get_class($guardAuthenticator)]); } diff --git a/src/Symfony/Component/Security/Guard/composer.json b/src/Symfony/Component/Security/Guard/composer.json index 63f0330f07625..a70886b02148c 100644 --- a/src/Symfony/Component/Security/Guard/composer.json +++ b/src/Symfony/Component/Security/Guard/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.2.5", "symfony/security-core": "^5.0", - "symfony/security-http": "^4.4|^5.0" + "symfony/security-http": "^4.4.1|^5.0.1" }, "require-dev": { "psr/log": "~1.0" diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php index b9fdb382b97c9..4fa09b30fc2df 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php @@ -48,7 +48,7 @@ * @author Fabien Potencier * @author Johannes M. Schmitt */ -abstract class AbstractAuthenticationListener +abstract class AbstractAuthenticationListener extends AbstractListener { protected $options; protected $logger; @@ -102,20 +102,24 @@ public function setRememberMeServices(RememberMeServicesInterface $rememberMeSer $this->rememberMeServices = $rememberMeServices; } + /** + * {@inheritdoc} + */ + public function supports(Request $request): ?bool + { + return $this->requiresAuthentication($request); + } + /** * Handles form based authentication. * * @throws \RuntimeException * @throws SessionUnavailableException */ - public function __invoke(RequestEvent $event) + public function authenticate(RequestEvent $event) { $request = $event->getRequest(); - if (!$this->requiresAuthentication($request)) { - return; - } - if (!$request->hasSession()) { throw new \RuntimeException('This authentication method requires a session.'); } diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php new file mode 100644 index 0000000000000..ecbfa30233eb5 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractListener.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Firewall; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; + +/** + * A base class for listeners that can tell whether they should authenticate incoming requests. + * + * @author Nicolas Grekas + */ +abstract class AbstractListener +{ + final public function __invoke(RequestEvent $event) + { + if (false !== $this->supports($event->getRequest())) { + $this->authenticate($event); + } + } + + /** + * Tells whether the authenticate() method should be called or not depending on the incoming request. + * + * Returning null means authenticate() can be called lazily when accessing the token storage. + */ + abstract public function supports(Request $request): ?bool; + + /** + * Does whatever is required to authenticate the request, typically calling $event->setResponse() internally. + */ + abstract public function authenticate(RequestEvent $event); +} diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php index b969ae3bdb1d0..d5c5d9325d1c4 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php @@ -34,7 +34,7 @@ * * @internal */ -abstract class AbstractPreAuthenticatedListener +abstract class AbstractPreAuthenticatedListener extends AbstractListener { protected $logger; private $tokenStorage; @@ -53,20 +53,31 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM } /** - * Handles pre-authentication. + * {@inheritdoc} */ - public function __invoke(RequestEvent $event) + public function supports(Request $request): ?bool { - $request = $event->getRequest(); - try { - list($user, $credentials) = $this->getPreAuthenticatedData($request); + $request->attributes->set('_pre_authenticated_data', $this->getPreAuthenticatedData($request)); } catch (BadCredentialsException $e) { $this->clearToken($e); - return; + return false; } + return true; + } + + /** + * Handles pre-authentication. + */ + public function authenticate(RequestEvent $event) + { + $request = $event->getRequest(); + + [$user, $credentials] = $request->attributes->get('_pre_authenticated_data'); + $request->attributes->remove('_pre_authenticated_data'); + if (null !== $this->logger) { $this->logger->debug('Checking current security token.', ['token' => (string) $this->tokenStorage->getToken()]); } diff --git a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php index 2dbeee1033151..3d88103d39abc 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php @@ -11,10 +11,12 @@ namespace Symfony\Component\Security\Http\Firewall; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; +use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; use Symfony\Component\Security\Http\AccessMapInterface; @@ -27,7 +29,7 @@ * * @final */ -class AccessListener +class AccessListener extends AbstractListener { private $tokenStorage; private $accessDecisionManager; @@ -42,13 +44,24 @@ public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionM $this->authManager = $authManager; } + /** + * {@inheritdoc} + */ + public function supports(Request $request): ?bool + { + [$attributes] = $this->map->getPatterns($request); + $request->attributes->set('_access_control_attributes', $attributes); + + return $attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes ? true : null; + } + /** * Handles access authorization. * * @throws AccessDeniedException * @throws AuthenticationCredentialsNotFoundException */ - public function __invoke(RequestEvent $event) + public function authenticate(RequestEvent $event) { if (!$event instanceof LazyResponseEvent && null === $token = $this->tokenStorage->getToken()) { throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.'); @@ -56,9 +69,10 @@ public function __invoke(RequestEvent $event) $request = $event->getRequest(); - list($attributes) = $this->map->getPatterns($request); + $attributes = $request->attributes->get('_access_control_attributes'); + $request->attributes->remove('_access_control_attributes'); - if (!$attributes) { + if (!$attributes || ([AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] === $attributes && $event instanceof LazyResponseEvent)) { return; } diff --git a/src/Symfony/Component/Security/Http/Firewall/AnonymousAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/AnonymousAuthenticationListener.php index f9f0ff0946400..c54e6ba67d8b4 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AnonymousAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AnonymousAuthenticationListener.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Http\Firewall; use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; @@ -26,7 +27,7 @@ * * @final */ -class AnonymousAuthenticationListener +class AnonymousAuthenticationListener extends AbstractListener { private $tokenStorage; private $secret; @@ -41,10 +42,18 @@ public function __construct(TokenStorageInterface $tokenStorage, string $secret, $this->logger = $logger; } + /** + * {@inheritdoc} + */ + public function supports(Request $request): ?bool + { + return null; // always run authenticate() lazily with lazy firewalls + } + /** * Handles anonymous authentication. */ - public function __invoke(RequestEvent $event) + public function authenticate(RequestEvent $event) { if (null !== $this->tokenStorage->getToken()) { return; diff --git a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php index 77c4f9dac96a9..0692e055d0539 100644 --- a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php @@ -29,7 +29,7 @@ * * @final */ -class BasicAuthenticationListener +class BasicAuthenticationListener extends AbstractListener { private $tokenStorage; private $authenticationManager; @@ -53,10 +53,18 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM $this->ignoreFailure = false; } + /** + * {@inheritdoc} + */ + public function supports(Request $request): ?bool + { + return null !== $request->headers->get('PHP_AUTH_USER'); + } + /** * Handles basic authentication. */ - public function __invoke(RequestEvent $event) + public function authenticate(RequestEvent $event) { $request = $event->getRequest(); diff --git a/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php b/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php index 94515061de88c..243faecbc083b 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ChannelListener.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Http\Firewall; use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Http\AccessMapInterface; use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; @@ -24,7 +25,7 @@ * * @final */ -class ChannelListener +class ChannelListener extends AbstractListener { private $map; private $authenticationEntryPoint; @@ -40,10 +41,8 @@ public function __construct(AccessMapInterface $map, AuthenticationEntryPointInt /** * Handles channel management. */ - public function __invoke(RequestEvent $event) + public function supports(Request $request): ?bool { - $request = $event->getRequest(); - list(, $channel) = $this->map->getPatterns($request); if ('https' === $channel && !$request->isSecure()) { @@ -57,11 +56,7 @@ public function __invoke(RequestEvent $event) } } - $response = $this->authenticationEntryPoint->start($request); - - $event->setResponse($response); - - return; + return true; } if ('http' === $channel && $request->isSecure()) { @@ -69,9 +64,18 @@ public function __invoke(RequestEvent $event) $this->logger->info('Redirecting to HTTP.'); } - $response = $this->authenticationEntryPoint->start($request); - - $event->setResponse($response); + return true; } + + return false; + } + + public function authenticate(RequestEvent $event) + { + $request = $event->getRequest(); + + $response = $this->authenticationEntryPoint->start($request); + + $event->setResponse($response); } } diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index b9f5809723d73..02de94628a396 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; @@ -29,6 +30,7 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Event\DeauthenticatedEvent; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -39,7 +41,7 @@ * * @final */ -class ContextListener +class ContextListener extends AbstractListener { private $tokenStorage; private $sessionKey; @@ -48,6 +50,7 @@ class ContextListener private $dispatcher; private $registered; private $trustResolver; + private $rememberMeServices; private $sessionTrackerEnabler; /** @@ -68,10 +71,18 @@ public function __construct(TokenStorageInterface $tokenStorage, iterable $userP $this->sessionTrackerEnabler = $sessionTrackerEnabler; } + /** + * {@inheritdoc} + */ + public function supports(Request $request): ?bool + { + return null; // always run authenticate() lazily with lazy firewalls + } + /** * Reads the Security Token from the session. */ - public function __invoke(RequestEvent $event) + public function authenticate(RequestEvent $event) { if (!$this->registered && null !== $this->dispatcher && $event->isMasterRequest()) { $this->dispatcher->addListener(KernelEvents::RESPONSE, [$this, 'onKernelResponse']); @@ -112,6 +123,10 @@ public function __invoke(RequestEvent $event) if ($token instanceof TokenInterface) { $token = $this->refreshUser($token); + + if (!$token && $this->rememberMeServices) { + $this->rememberMeServices->loginFail($request); + } } elseif (null !== $token) { if (null !== $this->logger) { $this->logger->warning('Expected a security token from the session, got something else.', ['key' => $this->sessionKey, 'received' => $token]); @@ -282,4 +297,9 @@ public static function handleUnserializeCallback($class) { throw new \ErrorException('Class not found: '.$class, 0x37313bc); } + + public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices) + { + $this->rememberMeServices = $rememberMeServices; + } } diff --git a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php index 46f757e1ee340..1194cea95f1e2 100644 --- a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php @@ -30,7 +30,7 @@ * * @final */ -class LogoutListener +class LogoutListener extends AbstractListener { private $tokenStorage; private $options; @@ -61,6 +61,14 @@ public function addHandler(LogoutHandlerInterface $handler) $this->handlers[] = $handler; } + /** + * {@inheritdoc} + */ + public function supports(Request $request): ?bool + { + return $this->requiresLogout($request); + } + /** * Performs the logout if requested. * @@ -70,14 +78,10 @@ public function addHandler(LogoutHandlerInterface $handler) * @throws LogoutException if the CSRF token is invalid * @throws \RuntimeException if the LogoutSuccessHandlerInterface instance does not return a response */ - public function __invoke(RequestEvent $event) + public function authenticate(RequestEvent $event) { $request = $event->getRequest(); - if (!$this->requiresLogout($request)) { - return; - } - if (null !== $this->csrfTokenManager) { $csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']); diff --git a/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php b/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php index 2d0626d05c501..c288fcb94b942 100644 --- a/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Http\Firewall; use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -30,7 +31,7 @@ * * @final */ -class RememberMeListener +class RememberMeListener extends AbstractListener { private $tokenStorage; private $rememberMeServices; @@ -51,10 +52,18 @@ public function __construct(TokenStorageInterface $tokenStorage, RememberMeServi $this->sessionStrategy = null === $sessionStrategy ? new SessionAuthenticationStrategy(SessionAuthenticationStrategy::MIGRATE) : $sessionStrategy; } + /** + * {@inheritdoc} + */ + public function supports(Request $request): ?bool + { + return null; // always run authenticate() lazily with lazy firewalls + } + /** * Handles remember-me cookie based authentication. */ - public function __invoke(RequestEvent $event) + public function authenticate(RequestEvent $event) { if (null !== $this->tokenStorage->getToken()) { return; diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index eadd23f91e4af..f6d4c8a587c0a 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -37,7 +37,7 @@ * * @final */ -class SwitchUserListener +class SwitchUserListener extends AbstractListener { const EXIT_VALUE = '_exit'; @@ -71,14 +71,10 @@ public function __construct(TokenStorageInterface $tokenStorage, UserProviderInt } /** - * Handles the switch to another user. - * - * @throws \LogicException if switching to a user failed + * {@inheritdoc} */ - public function __invoke(RequestEvent $event) + public function supports(Request $request): ?bool { - $request = $event->getRequest(); - // usernames can be falsy $username = $request->get($this->usernameParameter); @@ -88,9 +84,26 @@ public function __invoke(RequestEvent $event) // if it's still "empty", nothing to do. if (null === $username || '' === $username) { - return; + return false; } + $request->attributes->set('_switch_user_username', $username); + + return true; + } + + /** + * Handles the switch to another user. + * + * @throws \LogicException if switching to a user failed + */ + public function authenticate(RequestEvent $event) + { + $request = $event->getRequest(); + + $username = $request->attributes->get('_switch_user_username'); + $request->attributes->remove('_switch_user_username'); + if (null === $this->tokenStorage->getToken()) { throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.'); } @@ -144,7 +157,6 @@ private function attemptSwitchUser(Request $request, string $username): ?TokenIn try { $this->provider->loadUserByUsername($nonExistentUsername); - throw new \LogicException('AuthenticationException expected'); } catch (AuthenticationException $e) { } } catch (AuthenticationException $e) { diff --git a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php index 5c5a35e435093..ace15f03d522a 100644 --- a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php @@ -43,7 +43,7 @@ * * @final */ -class UsernamePasswordJsonAuthenticationListener +class UsernamePasswordJsonAuthenticationListener extends AbstractListener { private $tokenStorage; private $authenticationManager; @@ -71,19 +71,27 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } - public function __invoke(RequestEvent $event) + public function supports(Request $request): ?bool { - $request = $event->getRequest(); if (false === strpos($request->getRequestFormat(), 'json') && false === strpos($request->getContentType(), 'json') ) { - return; + return false; } if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) { - return; + return false; } + return true; + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestEvent $event) + { + $request = $event->getRequest(); $data = json_decode($request->getContent()); try { diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php index 1dff48dfda84f..168e25643705b 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; @@ -26,7 +27,7 @@ class AccessListenerTest extends TestCase public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess() { $this->expectException('Symfony\Component\Security\Core\Exception\AccessDeniedException'); - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock(); + $request = new Request(); $accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock(); $accessMap @@ -65,19 +66,12 @@ public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess() $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock() ); - $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); - $event - ->expects($this->any()) - ->method('getRequest') - ->willReturn($request) - ; - - $listener($event); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); } public function testHandleWhenTheTokenIsNotAuthenticated() { - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock(); + $request = new Request(); $accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock(); $accessMap @@ -136,19 +130,12 @@ public function testHandleWhenTheTokenIsNotAuthenticated() $authManager ); - $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); - $event - ->expects($this->any()) - ->method('getRequest') - ->willReturn($request) - ; - - $listener($event); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); } public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest() { - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock(); + $request = new Request(); $accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock(); $accessMap @@ -178,19 +165,12 @@ public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest() $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock() ); - $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); - $event - ->expects($this->any()) - ->method('getRequest') - ->willReturn($request) - ; - - $listener($event); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); } public function testHandleWhenAccessMapReturnsEmptyAttributes() { - $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->disableOriginalClone()->getMock(); + $request = new Request(); $accessMap = $this->getMockBuilder(AccessMapInterface::class)->getMock(); $accessMap @@ -213,12 +193,7 @@ public function testHandleWhenAccessMapReturnsEmptyAttributes() $this->getMockBuilder(AuthenticationManagerInterface::class)->getMock() ); - $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); - $event - ->expects($this->any()) - ->method('getRequest') - ->willReturn($request) - ; + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST); $listener(new LazyResponseEvent($event)); } @@ -233,7 +208,7 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken() ->willReturn(null) ; - $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->disableOriginalClone()->getMock(); + $request = new Request(); $accessMap = $this->getMockBuilder(AccessMapInterface::class)->getMock(); $accessMap @@ -250,13 +225,6 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken() $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock() ); - $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); - $event - ->expects($this->any()) - ->method('getRequest') - ->willReturn($request) - ; - - $listener($event); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST)); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php index 47f09199c43e5..e6f9f42217efb 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AnonymousAuthenticationListenerTest.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Security\Http\Tests\Firewall; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Http\Firewall\AnonymousAuthenticationListener; @@ -38,7 +40,7 @@ public function testHandleWithTokenStorageHavingAToken() ; $listener = new AnonymousAuthenticationListener($tokenStorage, 'TheSecret', null, $authenticationManager); - $listener($this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock()); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST)); } public function testHandleWithTokenStorageHavingNoToken() @@ -69,7 +71,7 @@ public function testHandleWithTokenStorageHavingNoToken() ; $listener = new AnonymousAuthenticationListener($tokenStorage, 'TheSecret', null, $authenticationManager); - $listener($this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock()); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST)); } public function testHandledEventIsLogged() @@ -84,6 +86,6 @@ public function testHandledEventIsLogged() $authenticationManager = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock(); $listener = new AnonymousAuthenticationListener($tokenStorage, 'TheSecret', $logger, $authenticationManager); - $listener($this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock()); + $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST)); } } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php index 3432bd7f97a6a..a6f118f6490a2 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php @@ -36,6 +36,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Event\DeauthenticatedEvent; use Symfony\Component\Security\Http\Firewall\ContextListener; +use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; use Symfony\Contracts\Service\ServiceLocatorTrait; class ContextListenerTest extends TestCase @@ -221,7 +222,7 @@ public function testOnKernelResponseListenerRemovesItself() ->willReturn(true); $request->expects($this->any()) ->method('getSession') - ->will($this->returnValue($session)); + ->willReturn($session); $event = new ResponseEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST, new Response()); @@ -262,10 +263,23 @@ public function testIfTokenIsNotDeauthenticated() $tokenStorage = new TokenStorage(); $badRefreshedUser = new User('foobar', 'baz'); $goodRefreshedUser = new User('foobar', 'bar'); - $tokenStorage = $this->handleEventWithPreviousSession([new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser, true); + $tokenStorage = $this->handleEventWithPreviousSession([new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser); $this->assertSame($goodRefreshedUser, $tokenStorage->getToken()->getUser()); } + public function testRememberMeGetsCanceledIfTokenIsDeauthenticated() + { + $tokenStorage = new TokenStorage(); + $refreshedUser = new User('foobar', 'baz'); + + $rememberMeServices = $this->createMock(RememberMeServicesInterface::class); + $rememberMeServices->expects($this->once())->method('loginFail'); + + $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)], null, $rememberMeServices); + + $this->assertNull($tokenStorage->getToken()); + } + public function testTryAllUserProvidersUntilASupportingUserProviderIsFound() { $refreshedUser = new User('foobar', 'baz'); @@ -372,7 +386,7 @@ protected function runSessionOnKernelResponse($newToken, $original = null) return $session; } - private function handleEventWithPreviousSession($userProviders, UserInterface $user = null) + private function handleEventWithPreviousSession($userProviders, UserInterface $user = null, RememberMeServicesInterface $rememberMeServices = null) { $user = $user ?: new User('foo', 'bar'); $session = new Session(new MockArraySessionStorage()); @@ -392,6 +406,10 @@ private function handleEventWithPreviousSession($userProviders, UserInterface $u $sessionTrackerEnabler = [$tokenStorage, 'enableUsageTracking']; $listener = new ContextListener($tokenStorage, $userProviders, 'context_key', null, null, null, $sessionTrackerEnabler); + + if ($rememberMeServices) { + $listener->setRememberMeServices($rememberMeServices); + } $listener(new RequestEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST)); $this->assertSame($usageIndex, $session->getUsageIndex()); diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php index ceb557b139d0a..d321ed68921bd 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/RememberMeListenerTest.php @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Firewall\RememberMeListener; use Symfony\Component\Security\Http\SecurityEvents; @@ -27,7 +28,7 @@ public function testOnCoreSecurityDoesNotTryToPopulateNonEmptyTokenStorage() list($listener, $tokenStorage) = $this->getListener(); $tokenStorage - ->expects($this->once()) + ->expects($this->any()) ->method('getToken') ->willReturn($this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock()) ; @@ -45,7 +46,7 @@ public function testOnCoreSecurityDoesNothingWhenNoCookieIsSet() list($listener, $tokenStorage, $service) = $this->getListener(); $tokenStorage - ->expects($this->once()) + ->expects($this->any()) ->method('getToken') ->willReturn(null) ; @@ -57,11 +58,6 @@ public function testOnCoreSecurityDoesNothingWhenNoCookieIsSet() ; $event = $this->getGetResponseEvent(); - $event - ->expects($this->once()) - ->method('getRequest') - ->willReturn(new Request()) - ; $this->assertNull($listener($event)); } @@ -73,7 +69,7 @@ public function testOnCoreSecurityIgnoresAuthenticationExceptionThrownByAuthenti $exception = new AuthenticationException('Authentication failed.'); $tokenStorage - ->expects($this->once()) + ->expects($this->any()) ->method('getToken') ->willReturn(null) ; @@ -96,12 +92,7 @@ public function testOnCoreSecurityIgnoresAuthenticationExceptionThrownByAuthenti ->willThrowException($exception) ; - $event = $this->getGetResponseEvent(); - $event - ->expects($this->once()) - ->method('getRequest') - ->willReturn($request) - ; + $event = $this->getGetResponseEvent($request); $listener($event); } @@ -113,7 +104,7 @@ public function testOnCoreSecurityIgnoresAuthenticationOptionallyRethrowsExcepti list($listener, $tokenStorage, $service, $manager) = $this->getListener(false, false); $tokenStorage - ->expects($this->once()) + ->expects($this->any()) ->method('getToken') ->willReturn(null) ; @@ -137,11 +128,6 @@ public function testOnCoreSecurityIgnoresAuthenticationOptionallyRethrowsExcepti ; $event = $this->getGetResponseEvent(); - $event - ->expects($this->once()) - ->method('getRequest') - ->willReturn(new Request()) - ; $listener($event); } @@ -151,7 +137,7 @@ public function testOnCoreSecurityAuthenticationExceptionDuringAutoLoginTriggers list($listener, $tokenStorage, $service, $manager) = $this->getListener(); $tokenStorage - ->expects($this->once()) + ->expects($this->any()) ->method('getToken') ->willReturn(null) ; @@ -174,11 +160,6 @@ public function testOnCoreSecurityAuthenticationExceptionDuringAutoLoginTriggers ; $event = $this->getGetResponseEvent(); - $event - ->expects($this->once()) - ->method('getRequest') - ->willReturn(new Request()) - ; $listener($event); } @@ -188,7 +169,7 @@ public function testOnCoreSecurity() list($listener, $tokenStorage, $service, $manager) = $this->getListener(); $tokenStorage - ->expects($this->once()) + ->expects($this->any()) ->method('getToken') ->willReturn(null) ; @@ -213,11 +194,6 @@ public function testOnCoreSecurity() ; $event = $this->getGetResponseEvent(); - $event - ->expects($this->once()) - ->method('getRequest') - ->willReturn(new Request()) - ; $listener($event); } @@ -227,7 +203,7 @@ public function testSessionStrategy() list($listener, $tokenStorage, $service, $manager, , , $sessionStrategy) = $this->getListener(false, true, true); $tokenStorage - ->expects($this->once()) + ->expects($this->any()) ->method('getToken') ->willReturn(null) ; @@ -258,25 +234,10 @@ public function testSessionStrategy() ->willReturn(true) ; - $request = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request')->getMock(); - $request - ->expects($this->once()) - ->method('hasSession') - ->willReturn(true) - ; - - $request - ->expects($this->once()) - ->method('getSession') - ->willReturn($session) - ; + $request = new Request(); + $request->setSession($session); - $event = $this->getGetResponseEvent(); - $event - ->expects($this->once()) - ->method('getRequest') - ->willReturn($request) - ; + $event = $this->getGetResponseEvent($request); $sessionStrategy ->expects($this->once()) @@ -292,7 +253,7 @@ public function testSessionIsMigratedByDefault() list($listener, $tokenStorage, $service, $manager) = $this->getListener(false, true, false); $tokenStorage - ->expects($this->once()) + ->expects($this->any()) ->method('getToken') ->willReturn(null) ; @@ -327,25 +288,10 @@ public function testSessionIsMigratedByDefault() ->method('migrate') ; - $request = $this->getMockBuilder('\Symfony\Component\HttpFoundation\Request')->getMock(); - $request - ->expects($this->any()) - ->method('hasSession') - ->willReturn(true) - ; + $request = new Request(); + $request->setSession($session); - $request - ->expects($this->any()) - ->method('getSession') - ->willReturn($session) - ; - - $event = $this->getGetResponseEvent(); - $event - ->expects($this->once()) - ->method('getRequest') - ->willReturn($request) - ; + $event = $this->getGetResponseEvent($request); $listener($event); } @@ -355,7 +301,7 @@ public function testOnCoreSecurityInteractiveLoginEventIsDispatchedIfDispatcherI list($listener, $tokenStorage, $service, $manager, , $dispatcher) = $this->getListener(true); $tokenStorage - ->expects($this->once()) + ->expects($this->any()) ->method('getToken') ->willReturn(null) ; @@ -380,12 +326,6 @@ public function testOnCoreSecurityInteractiveLoginEventIsDispatchedIfDispatcherI ; $event = $this->getGetResponseEvent(); - $request = new Request(); - $event - ->expects($this->once()) - ->method('getRequest') - ->willReturn($request) - ; $dispatcher ->expects($this->once()) @@ -399,9 +339,20 @@ public function testOnCoreSecurityInteractiveLoginEventIsDispatchedIfDispatcherI $listener($event); } - protected function getGetResponseEvent() + protected function getGetResponseEvent(Request $request = null): RequestEvent { - return $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); + $request = $request ?? new Request(); + + $event = $this->getMockBuilder(RequestEvent::class) + ->setConstructorArgs([$this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST]) + ->getMock(); + $event + ->expects($this->any()) + ->method('getRequest') + ->willReturn($request) + ; + + return $event; } protected function getResponseEvent(): ResponseEvent diff --git a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php index 133f3d562bc00..183e02fce1e7a 100644 --- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php @@ -37,7 +37,6 @@ class CsvEncoder implements EncoderInterface, DecoderInterface private $formulasStartCharacters = ['=', '-', '+', '@']; private $defaultContext = [ - self::AS_COLLECTION_KEY => true, self::DELIMITER_KEY => ',', self::ENCLOSURE_KEY => '"', self::ESCAPE_CHAR_KEY => '', @@ -45,6 +44,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface self::HEADERS_KEY => [], self::KEY_SEPARATOR_KEY => '.', self::NO_HEADERS_KEY => false, + self::AS_COLLECTION_KEY => true, self::OUTPUT_UTF8_BOM_KEY => false, ]; @@ -92,7 +92,7 @@ public function encode($data, string $format, array $context = []) $headers = array_merge(array_values($headers), array_diff($this->extractHeaders($data), $headers)); - if (!($context[self::NO_HEADERS_KEY] ?? false)) { + if (!($context[self::NO_HEADERS_KEY] ?? $this->defaultContext[self::NO_HEADERS_KEY])) { fputcsv($handle, $headers, $delimiter, $enclosure, $escapeChar); } @@ -150,7 +150,7 @@ public function decode(string $data, string $format, array $context = []) if (null === $headers) { $nbHeaders = $nbCols; - if ($context[self::NO_HEADERS_KEY] ?? false) { + if ($context[self::NO_HEADERS_KEY] ?? $this->defaultContext[self::NO_HEADERS_KEY]) { for ($i = 0; $i < $nbCols; ++$i) { $headers[] = [$i]; } diff --git a/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php b/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php index 15b535b0c73e4..b32a04b5a92a0 100644 --- a/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php +++ b/src/Symfony/Component/Serializer/Mapping/Factory/ClassResolverTrait.php @@ -27,7 +27,7 @@ trait ClassResolverTrait * * @param object|string $value * - * @throws InvalidArgumentException If the class does not exists + * @throws InvalidArgumentException If the class does not exist */ private function getClass($value): string { diff --git a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php index 86626490f0ccc..80840a042f2fc 100644 --- a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php +++ b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php @@ -123,7 +123,7 @@ private function getCacheValueForAttributesMetadata(string $class, array $contex if (!$groups && ($context[AbstractNormalizer::GROUPS] ?? [])) { continue; } - if ($groups && !array_intersect($groups, $context[AbstractNormalizer::GROUPS] ?? [])) { + if ($groups && !array_intersect($groups, (array) ($context[AbstractNormalizer::GROUPS] ?? []))) { continue; } diff --git a/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php index 17d599d74e215..e2ec20b881f2a 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php @@ -41,14 +41,15 @@ public function __construct(bool $debug = false, array $defaultContext = []) public function normalize($exception, string $format = null, array $context = []) { $context += $this->defaultContext; + $debug = $this->debug && $context['debug'] ?? true; $data = [ 'type' => $context['type'], 'title' => $context['title'], 'status' => $context['status'] ?? $exception->getStatusCode(), - 'detail' => $this->debug ? $exception->getMessage() : $exception->getStatusText(), + 'detail' => $debug ? $exception->getMessage() : $exception->getStatusText(), ]; - if ($this->debug) { + if ($debug) { $data['class'] = $exception->getClass(); $data['trace'] = $exception->getTrace(); } diff --git a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php index 29fd0f625f515..1b77704ef98ae 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php @@ -189,6 +189,24 @@ public function testEncodeCustomSettingsPassedInContext() ])); } + public function testEncodeCustomSettingsPassedInConstructor() + { + $encoder = new CsvEncoder([ + CsvEncoder::DELIMITER_KEY => ';', + CsvEncoder::ENCLOSURE_KEY => "'", + CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::KEY_SEPARATOR_KEY => '-', + ]); + $value = ['a' => 'he\'llo', 'c' => ['d' => 'foo']]; + + $this->assertSame(<<<'CSV' +a;c-d +'he''llo';foo + +CSV + , $encoder->encode($value, 'csv')); + } + public function testEncodeEmptyArray() { $this->assertEquals("\n\n", $this->encoder->encode([], 'csv')); @@ -346,6 +364,15 @@ public function testEncodeWithoutHeader() , $this->encoder->encode([['a', 'b'], ['c', 'd']], 'csv', [ CsvEncoder::NO_HEADERS_KEY => true, ])); + $encoder = new CsvEncoder([CsvEncoder::NO_HEADERS_KEY => true]); + $this->assertSame(<<<'CSV' +a,b +c,d + +CSV + , $encoder->encode([['a', 'b'], ['c', 'd']], 'csv', [ + CsvEncoder::NO_HEADERS_KEY => true, + ])); } public function testEncodeArrayObject() @@ -500,6 +527,23 @@ public function testDecodeCustomSettingsPassedInContext() ])); } + public function testDecodeCustomSettingsPassedInConstructor() + { + $encoder = new CsvEncoder([ + CsvEncoder::DELIMITER_KEY => ';', + CsvEncoder::ENCLOSURE_KEY => "'", + CsvEncoder::ESCAPE_CHAR_KEY => '|', + CsvEncoder::KEY_SEPARATOR_KEY => '-', + CsvEncoder::AS_COLLECTION_KEY => true, // Can be removed in 5.0 + ]); + $expected = [['a' => 'hell\'o', 'bar' => ['baz' => 'b']]]; + $this->assertEquals($expected, $encoder->decode(<<<'CSV' +a;bar-baz +'hell''o';b;c +CSV + , 'csv')); + } + public function testDecodeMalformedCollection() { $expected = [ @@ -529,6 +573,15 @@ public function testDecodeWithoutHeader() a,b c,d +CSV + , 'csv', [ + CsvEncoder::NO_HEADERS_KEY => true, + ])); + $encoder = new CsvEncoder([CsvEncoder::NO_HEADERS_KEY => true]); + $this->assertEquals([['a', 'b'], ['c', 'd']], $encoder->decode(<<<'CSV' +a,b +c,d + CSV , 'csv', [ CsvEncoder::NO_HEADERS_KEY => true, diff --git a/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php b/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php index 7ea71ffecacd1..9b0ad4d79c55b 100644 --- a/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php +++ b/src/Symfony/Component/Serializer/Tests/NameConverter/MetadataAwareNameConverterTest.php @@ -146,6 +146,8 @@ public function attributeAndContextProvider() return [ ['buz', 'buz', ['groups' => ['a']]], ['buzForExport', 'buz', ['groups' => ['b']]], + ['buz', 'buz', ['groups' => 'a']], + ['buzForExport', 'buz', ['groups' => 'b']], ['buz', 'buz', ['groups' => ['c']]], ['buz', 'buz', []], ]; diff --git a/src/Symfony/Component/Validator/Constraint.php b/src/Symfony/Component/Validator/Constraint.php index 224c7edd412e5..b349d8675d847 100644 --- a/src/Symfony/Component/Validator/Constraint.php +++ b/src/Symfony/Component/Validator/Constraint.php @@ -287,7 +287,7 @@ public function getTargets() * * @internal */ - public function __sleep(): array + public function __sleep() { // Initialize "groups" option if it is not set $this->groups; diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf index 5a391a2e6626e..21e2392c7d96c 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ja.xlf @@ -362,6 +362,10 @@ This password has been leaked in a data breach, it must not be used. Please use another password. このパスワードは漏洩している為使用できません。 + + This value should be between {{ min }} and {{ max }}. + {{ min }}以上{{ max }}以下でなければなりません。 + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.sl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.sl.xlf index 6f5fd98ca192e..cb12a8a9daa4d 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.sl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.sl.xlf @@ -318,6 +318,54 @@ Error Napaka + + This is not a valid UUID. + To ni veljaven UUID. + + + This value should be a multiple of {{ compared_value }}. + Ta vrednost bi morala biti večkratnik od {{ compared_value }}. + + + This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}. + Ta poslovna identifikacijska koda (BIC) ni povezana z IBAN {{ iban }}. + + + This value should be valid JSON. + Ta vrednost bi morala biti veljaven JSON. + + + This collection should contain only unique elements. + Ta zbirka bi morala vsebovati samo edinstvene elemente. + + + This value should be positive. + Ta vrednost bi morala biti pozitivna. + + + This value should be either positive or zero. + Ta vrednost bi morala biti pozitivna ali enaka nič. + + + This value should be negative. + Ta vrednost bi morala biti negativna. + + + This value should be either negative or zero. + Ta vrednost bi morala biti negativna ali enaka nič. + + + This value is not a valid timezone. + Ta vrednost ni veljaven časovni pas. + + + This password has been leaked in a data breach, it must not be used. Please use another password. + To geslo je ušlo pri kršitvi varnosti podatkov in ga ne smete uporabljati. Prosimo, uporabite drugo geslo. + + + This value should be between {{ min }} and {{ max }}. + Ta vrednost bi morala biti med {{ min }} in {{ max }}. + diff --git a/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php b/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php index 3db44b150a58c..8e4fc6ba1bcb2 100644 --- a/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php @@ -177,7 +177,8 @@ protected function expectValidateAt($i, $propertyPath, $value, $group) ->willReturn($validator); $validator->expects($this->at(2 * $i + 1)) ->method('validate') - ->with($value, $this->logicalOr(null, [], $this->isInstanceOf('\Symfony\Component\Validator\Constraints\Valid')), $group); + ->with($value, $this->logicalOr(null, [], $this->isInstanceOf('\Symfony\Component\Validator\Constraints\Valid')), $group) + ->willReturn($validator); } protected function expectValidateValueAt($i, $propertyPath, $value, $constraints, $group = null) @@ -189,7 +190,8 @@ protected function expectValidateValueAt($i, $propertyPath, $value, $constraints ->willReturn($contextualValidator); $contextualValidator->expects($this->at(2 * $i + 1)) ->method('validate') - ->with($value, $constraints, $group); + ->with($value, $constraints, $group) + ->willReturn($contextualValidator); } protected function assertNoViolation() diff --git a/src/Symfony/Component/VarDumper/Dumper/ContextProvider/SourceContextProvider.php b/src/Symfony/Component/VarDumper/Dumper/ContextProvider/SourceContextProvider.php index e43e19f4356f6..6f4caba63b989 100644 --- a/src/Symfony/Component/VarDumper/Dumper/ContextProvider/SourceContextProvider.php +++ b/src/Symfony/Component/VarDumper/Dumper/ContextProvider/SourceContextProvider.php @@ -52,8 +52,8 @@ public function getContext(): ?array && 'dump' === $trace[$i]['function'] && VarDumper::class === $trace[$i]['class'] ) { - $file = $trace[$i]['file']; - $line = $trace[$i]['line']; + $file = $trace[$i]['file'] ?? $file; + $line = $trace[$i]['line'] ?? $line; while (++$i < $this->limit) { if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && 0 !== strpos($trace[$i]['function'], 'call_user_func')) { diff --git a/src/Symfony/Component/VarExporter/Internal/Exporter.php b/src/Symfony/Component/VarExporter/Internal/Exporter.php index e2eb57bc9b865..4d83a4b094dba 100644 --- a/src/Symfony/Component/VarExporter/Internal/Exporter.php +++ b/src/Symfony/Component/VarExporter/Internal/Exporter.php @@ -222,14 +222,14 @@ public static function export($value, string $indent = '') )); if ("'" === $m[2]) { - return substr($m[1], 0, -2); + return substr($m[1], 0, -2); } if ('n".\'' === substr($m[1], -4)) { - return substr_replace($m[1], "\n".$subIndent.".'".$m[2], -2); + return substr_replace($m[1], "\n".$subIndent.".'".$m[2], -2); } - return $m[1].$m[2]; + return $m[1].$m[2]; }, $code, -1, $count); if ($count && 0 === strpos($code, "''.")) { diff --git a/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php b/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php index 3ef65b315afa8..39d2ccec21844 100644 --- a/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php +++ b/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php @@ -51,7 +51,7 @@ public function getMarking(object $subject): Marking $method = 'get'.ucfirst($this->property); if (!method_exists($subject, $method)) { - throw new LogicException(sprintf('The method "%s::%s()" does not exists.', \get_class($subject), $method)); + throw new LogicException(sprintf('The method "%s::%s()" does not exist.', \get_class($subject), $method)); } $marking = $subject->{$method}(); @@ -81,7 +81,7 @@ public function setMarking(object $subject, Marking $marking, array $context = [ $method = 'set'.ucfirst($this->property); if (!method_exists($subject, $method)) { - throw new LogicException(sprintf('The method "%s::%s()" does not exists.', \get_class($subject), $method)); + throw new LogicException(sprintf('The method "%s::%s()" does not exist.', \get_class($subject), $method)); } $subject->{$method}($marking, $context); diff --git a/src/Symfony/Component/Workflow/Tests/StateMachineTest.php b/src/Symfony/Component/Workflow/Tests/StateMachineTest.php index 9224f7cb129d9..a6c7362f79568 100644 --- a/src/Symfony/Component/Workflow/Tests/StateMachineTest.php +++ b/src/Symfony/Component/Workflow/Tests/StateMachineTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Workflow\Event\GuardEvent; +use Symfony\Component\Workflow\Exception\NotEnabledTransitionException; use Symfony\Component\Workflow\StateMachine; use Symfony\Component\Workflow\TransitionBlocker; @@ -84,27 +85,52 @@ public function testBuildTransitionBlockerListReturnsExpectedReasonOnBranchMerge $subject = new Subject(); // There may be multiple transitions with the same name. Make sure that transitions - // that are not enabled by the marking are evaluated. + // that are enabled by the marking are evaluated. // see https://github.com/symfony/symfony/issues/28432 - // Test if when you are in place "a"trying transition "t1" then returned + // Test if when you are in place "a" and trying to apply "t1" then it returns // blocker list contains guard blocker instead blockedByMarking $subject->setMarking('a'); $transitionBlockerList = $net->buildTransitionBlockerList($subject, 't1'); $this->assertCount(1, $transitionBlockerList); $blockers = iterator_to_array($transitionBlockerList); - $this->assertSame('Transition blocker of place a', $blockers[0]->getMessage()); $this->assertSame('blocker', $blockers[0]->getCode()); - // Test if when you are in place "d" trying transition "t1" then - // returned blocker list contains guard blocker instead blockedByMarking + // Test if when you are in place "d" and trying to apply "t1" then + // it returns blocker list contains guard blocker instead blockedByMarking $subject->setMarking('d'); $transitionBlockerList = $net->buildTransitionBlockerList($subject, 't1'); $this->assertCount(1, $transitionBlockerList); $blockers = iterator_to_array($transitionBlockerList); - $this->assertSame('Transition blocker of place d', $blockers[0]->getMessage()); $this->assertSame('blocker', $blockers[0]->getCode()); } + + public function testApplyReturnsExpectedReasonOnBranchMerge() + { + $definition = $this->createComplexStateMachineDefinition(); + + $dispatcher = new EventDispatcher(); + $net = new StateMachine($definition, null, $dispatcher); + + $dispatcher->addListener('workflow.guard', function (GuardEvent $event) { + $event->addTransitionBlocker(new TransitionBlocker(sprintf('Transition blocker of place %s', $event->getTransition()->getFroms()[0]), 'blocker')); + }); + + $subject = new Subject(); + + // There may be multiple transitions with the same name. Make sure that all transitions + // that are enabled by the marking are evaluated. + // see https://github.com/symfony/symfony/issues/34489 + + try { + $net->apply($subject, 't1'); + $this->fail(); + } catch (NotEnabledTransitionException $e) { + $blockers = iterator_to_array($e->getTransitionBlockerList()); + $this->assertSame('Transition blocker of place a', $blockers[0]->getMessage()); + $this->assertSame('blocker', $blockers[0]->getCode()); + } + } } diff --git a/src/Symfony/Component/Workflow/Workflow.php b/src/Symfony/Component/Workflow/Workflow.php index 6d630d779206d..09418d48fb0e7 100644 --- a/src/Symfony/Component/Workflow/Workflow.php +++ b/src/Symfony/Component/Workflow/Workflow.php @@ -154,25 +154,47 @@ public function apply(object $subject, string $transitionName, array $context = { $marking = $this->getMarking($subject); - $transitionBlockerList = null; - $applied = false; - $approvedTransitionQueue = []; + $transitionExist = false; + $approvedTransitions = []; + $bestTransitionBlockerList = null; foreach ($this->definition->getTransitions() as $transition) { if ($transition->getName() !== $transitionName) { continue; } - $transitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition); - if (!$transitionBlockerList->isEmpty()) { + $transitionExist = true; + + $tmpTransitionBlockerList = $this->buildTransitionBlockerListForTransition($subject, $marking, $transition); + + if ($tmpTransitionBlockerList->isEmpty()) { + $approvedTransitions[] = $transition; + continue; + } + + if (!$bestTransitionBlockerList) { + $bestTransitionBlockerList = $tmpTransitionBlockerList; continue; } - $approvedTransitionQueue[] = $transition; + + // We prefer to return transitions blocker by something else than + // marking. Because it means the marking was OK. Transitions are + // deterministic: it's not possible to have many transitions enabled + // at the same time that match the same marking with the same name + if (!$tmpTransitionBlockerList->has(TransitionBlocker::BLOCKED_BY_MARKING)) { + $bestTransitionBlockerList = $tmpTransitionBlockerList; + } + } + + if (!$transitionExist) { + throw new UndefinedTransitionException($subject, $transitionName, $this); } - foreach ($approvedTransitionQueue as $transition) { - $applied = true; + if (!$approvedTransitions) { + throw new NotEnabledTransitionException($subject, $transitionName, $this, $bestTransitionBlockerList); + } + foreach ($approvedTransitions as $transition) { $this->leave($subject, $transition, $marking); $context = $this->transition($subject, $transition, $marking, $context); @@ -188,14 +210,6 @@ public function apply(object $subject, string $transitionName, array $context = $this->announce($subject, $transition, $marking); } - if (!$transitionBlockerList) { - throw new UndefinedTransitionException($subject, $transitionName, $this); - } - - if (!$applied) { - throw new NotEnabledTransitionException($subject, $transitionName, $this, $transitionBlockerList); - } - return $marking; } diff --git a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php index d3c4f0f1db57b..96486ca3168c8 100644 --- a/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php +++ b/src/Symfony/Contracts/HttpClient/Test/Fixtures/web/index.php @@ -155,6 +155,27 @@ usleep(500); } exit; + + case '/json': + header("Content-Type: application/json"); + echo json_encode([ + 'documents' => [ + ['id' => '/json/1'], + ['id' => '/json/2'], + ['id' => '/json/3'], + ], + ]); + exit; + + case '/json/1': + case '/json/2': + case '/json/3': + header("Content-Type: application/json"); + echo json_encode([ + 'title' => $vars['REQUEST_URI'], + ]); + + exit; } header('Content-Type: application/json', true); diff --git a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php index 4badb4c358909..aad712e3e8dcd 100644 --- a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php +++ b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php @@ -24,8 +24,6 @@ */ abstract class HttpClientTestCase extends TestCase { - private static $server; - public static function setUpBeforeClass(): void { TestHttpServer::start(); diff --git a/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php b/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php index 8e7a469c42603..0adb1a52a3036 100644 --- a/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php +++ b/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php @@ -19,31 +19,22 @@ */ class TestHttpServer { - private static $server; + private static $started; public static function start() { - if (null !== self::$server) { + if (self::$started) { return; } $finder = new PhpExecutableFinder(); $process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', '127.0.0.1:8057'])); $process->setWorkingDirectory(__DIR__.'/Fixtures/web'); - $process->setTimeout(300); $process->start(); - self::$server = new class() { - public $process; - - public function __destruct() - { - $this->process->stop(); - } - }; - - self::$server->process = $process; - + register_shutdown_function([$process, 'stop']); sleep('\\' === \DIRECTORY_SEPARATOR ? 10 : 1); + + self::$started = true; } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy