diff --git a/.travis.yml b/.travis.yml index 691f8ee2d6d7d..7c32c08b75e3b 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 @@ -261,6 +267,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 @@ -308,7 +319,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 64e77d4a4d718..7bd4ac031e861 100644 --- a/CHANGELOG-4.4.md +++ b/CHANGELOG-4.4.md @@ -7,6 +7,58 @@ 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.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 #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 #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) + * 4.4.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/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index c5f34ce951a21..f25362d833871 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -14,7 +14,6 @@ use Doctrine\Common\Collections\Collection; use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\Common\Persistence\ObjectManager; -use Doctrine\ORM\QueryBuilder; use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader; @@ -85,13 +84,16 @@ public static function createChoiceName($choice, $key, $value): string * 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; } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 87d131a8c2ed8..7d3f100663254 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -53,6 +53,10 @@ public function configureOptions(OptionsResolver $resolver) */ public function getLoader(ObjectManager $manager, $queryBuilder, $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); } @@ -68,11 +72,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 abd7cc57b33d4..4d722c46ecfcf 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php @@ -41,7 +41,7 @@ public function onKernelResponse(FilterResponseEvent $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(FilterResponseEvent $event) */ protected function sendHeader($header, $content) { - if (!$this->sendHeaders) { + if (!self::$sendHeaders) { return; } 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 225d80662a3dd..dcc704f3d0975 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 @@ -101,31 +100,23 @@ public function getLegacyExtractData() /** * @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 b7c787226656f..107d8cc4bff17 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/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index d8f06f87bd133..20df0ba123723 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -26,7 +26,7 @@ "symfony/dependency-injection": "^3.4|^4.0|^5.0", "symfony/error-handler": "^4.4|^5.0", "symfony/finder": "^3.4|^4.0|^5.0", - "symfony/form": "^4.4|^5.0", + "symfony/form": "^4.3.5", "symfony/http-foundation": "^4.3|^5.0", "symfony/http-kernel": "^4.4", "symfony/mime": "^4.3|^5.0", diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 0220844b2697d..7a7e049dcfd08 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -18,7 +18,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 c99106299a588..827b306bcfefa 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; @@ -1014,6 +1015,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) { @@ -1172,6 +1174,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 a56a05e741241..1832d962d287c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -304,7 +304,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'); @@ -1644,7 +1644,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]); @@ -1696,7 +1696,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".'); 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 a95e649c80c91..5356ffe486c11 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -329,6 +329,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 d0fb6ee0daa28..db4b2504aa3ec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php @@ -49,6 +49,6 @@ public function process(ContainerBuilder $container) } }); - $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 7022e25a21b88..369789ba3cbca 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": "^4.3.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4.1|^5.0.1", + "symfony/error-handler": "^4.4.1|^5.0.1", "symfony/http-foundation": "^4.4|^5.0", "symfony/http-kernel": "^4.4", "symfony/polyfill-mbstring": "~1.0", @@ -39,7 +40,7 @@ "symfony/dom-crawler": "^4.3|^5.0", "symfony/dotenv": "^4.3.6|^5.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/form": "^4.3.4|^5.0", + "symfony/form": "^4.3.5|^5.0", "symfony/expression-language": "^3.4|^4.0|^5.0", "symfony/http-client": "^4.4|^5.0", "symfony/lock": "^4.4|^5.0", @@ -72,7 +73,7 @@ "symfony/dotenv": "<4.3.6", "symfony/dom-crawler": "<4.3", "symfony/http-client": "<4.4", - "symfony/form": "<4.3", + "symfony/form": "<4.3.5", "symfony/lock": "<4.4", "symfony/mailer": "<4.4", "symfony/messenger": "<4.4", diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php index 36b01fda12fbd..0bc7fdda9e573 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php @@ -50,7 +50,7 @@ public function __invoke(RequestEvent $event) if (\is_callable($this->listener)) { ($this->listener)($event); } else { - @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($this->listener)), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, extend "%s" instead.', \get_class($this->listener), AbstractListener::class), E_USER_DEPRECATED); $this->listener->handle($event); } $this->time = microtime(true) - $startTime; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index 28103a3522542..90d771d825d49 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, $id, $config, $userProvider, 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 480768a6d07fc..471f820c2199b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -320,10 +320,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; @@ -396,7 +397,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); @@ -409,9 +410,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'])); @@ -449,7 +448,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; @@ -464,9 +463,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 410646d9ba5d6..2ea2c3fa7d732 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -156,9 +156,7 @@ - - diff --git a/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php index ef9b1e217cd5a..a45cc9c6d667c 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,26 +41,41 @@ public function getListeners(): iterable public function __invoke(RequestEvent $event) { - $this->tokenStorage->setInitializer(function () use ($event) { - $event = new LazyResponseEvent($event); - foreach (parent::getListeners() as $listener) { - if (\is_callable($listener)) { - $listener($event); - } else { - @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($listener)), E_USER_DEPRECATED); - $listener->handle($event); - } + $listeners = []; + $request = $event->getRequest(); + $lazy = $request->isMethodCacheable(); + + foreach (parent::getListeners() as $listener) { + if (!\is_callable($listener)) { + @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, extend "%s" instead.', \get_class($listener), AbstractListener::class), E_USER_DEPRECATED); + $listeners[] = [$listener, 'handle']; + $lazy = false; + } elseif (!$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 98624f747818e..7c84f953a55d8 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 4ec8665479300..4093fe2b94b84 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -24,7 +24,7 @@ "symfony/security-core": "^4.4", "symfony/security-csrf": "^4.2|^5.0", "symfony/security-guard": "^4.2|^5.0", - "symfony/security-http": "^4.4" + "symfony/security-http": "^4.4.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 c6b0aaa5846cd..76665764a19f5 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; @@ -119,6 +120,10 @@ public function process(ContainerBuilder $container) $loader = $container->getDefinition('twig.loader.filesystem'); $loader->setMethodCalls(array_merge($twigLoader->getMethodCalls(), $loader->getMethodCalls())); + if (!method_exists(AssetExtension::class, 'getName')) { + $container->removeDefinition('templating.engine.twig'); + } + $twigLoader->clearTag('twig.loader'); } else { $container->setAlias('twig.loader.filesystem', new Alias('twig.loader.native_filesystem', false)); diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php index 40d257c335b24..f38fd6fa58ee6 100644 --- a/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php @@ -121,7 +121,7 @@ protected function execute(InputInterface $input, OutputInterface $output) continue; } - $this->displayLog($input, $output, $clientId, $record); + $this->displayLog($output, $clientId, $record); } return 0; @@ -150,7 +150,7 @@ private function getLogs($socket): iterable } } - private function displayLog(InputInterface $input, OutputInterface $output, int $clientId, array $record) + private function displayLog(OutputInterface $output, int $clientId, array $record) { if (isset($record['log_id'])) { $clientId = unpack('H*', $record['log_id'])[1]; diff --git a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php index 71ace29d1c88b..5f6e99169c358 100644 --- a/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/PhpArrayAdapter.php @@ -305,7 +305,7 @@ private function generateItems(array $keys): \Generator public static function throwOnRequiredClass($class) { $e = new \ReflectionException("Class $class does not exist"); - $trace = $e->getTrace(); + $trace = debug_backtrace(); $autoloadFrame = [ 'function' => 'spl_autoload_call', 'args' => [$class], diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 5eab66890b380..9b65ccb987f0e 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 0d722c6bd28a8..ad70d6e0e08cb 100644 --- a/src/Symfony/Component/Config/Definition/VariableNode.php +++ b/src/Symfony/Component/Config/Definition/VariableNode.php @@ -85,7 +85,7 @@ protected function finalizeValue($value) // this avoids ever passing an empty value to final validation closures if (!$this->allowEmptyValue && $this->isHandlingPlaceholder() && $this->finalValidationClosures) { @trigger_error(sprintf('Setting path "%s" to an environment variable is deprecated since Symfony 4.3. Remove "cannotBeEmpty()", "validate()" or include a prefix/suffix value instead.', $this->getPath()), E_USER_DEPRECATED); -// $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 75b14c6b731ab..ba50c62cd61d3 100644 --- a/src/Symfony/Component/Config/Resource/ClassExistenceResource.php +++ b/src/Symfony/Component/Config/Resource/ClassExistenceResource.php @@ -72,7 +72,7 @@ public function isFresh($timestamp) 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($class, \Exception $previous = null) throw $e; } - $trace = $e->getTrace(); + $trace = debug_backtrace(); $autoloadFrame = [ 'function' => 'spl_autoload_call', 'args' => [$class], @@ -183,15 +183,17 @@ public static function throwOnRequiredClass($class, \Exception $previous = null) } $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 3ac30b46634cd..48fdcd69b056c 100644 --- a/src/Symfony/Component/Config/Resource/ReflectionClassResource.php +++ b/src/Symfony/Component/Config/Resource/ReflectionClassResource.php @@ -141,12 +141,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 6ffb461a457bb..413a495825e1d 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() 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 82c56f4689d5d..25d2b750b4aae 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/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index 7e2e97848181c..9e071fc7600a3 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -459,7 +459,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/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 a474613ef7bb0..cc39fd4e6c99b 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) { @@ -116,12 +117,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 0efa1239f0705..f1477ecfd712d 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 5733eb11ca370..41a9f0a3ac933 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 18a44b980689f..ee8c1401513c0 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 7ffc2b49c37f4..aefde3a4734ac 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services8.php @@ -112,6 +112,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 970cf416b3442..e0e5928914e6b 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, $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 2010e36ababb5..fc827df18cd4f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -406,6 +406,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() @@ -798,12 +801,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 de40ade592d2e..81bb08810eaac 100644 --- a/src/Symfony/Component/Dotenv/Dotenv.php +++ b/src/Symfony/Component/Dotenv/Dotenv.php @@ -466,7 +466,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 3f232698aebf7..d4c7c53dcd2ba 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 a09d8d57c6357..cd419098cc828 100644 --- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -724,7 +724,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/EventDispatcher/EventDispatcher.php b/src/Symfony/Component/EventDispatcher/EventDispatcher.php index 816a0ea904cb4..9a2459cdc6ca7 100644 --- a/src/Symfony/Component/EventDispatcher/EventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/EventDispatcher.php @@ -54,7 +54,7 @@ public function dispatch($event/*, string $eventName = null*/) if (\is_object($event)) { $eventName = $eventName ?? \get_class($event); - } elseif (\is_string($event) && (null === $eventName || $eventName instanceof Event)) { + } elseif (\is_string($event) && (null === $eventName || $eventName instanceof ContractsEvent || $eventName instanceof Event)) { @trigger_error(sprintf('Calling the "%s::dispatch()" method with the event name as the first argument is deprecated since Symfony 4.3, pass it as the second argument and provide the event object as the first argument instead.', EventDispatcherInterface::class), E_USER_DEPRECATED); $swap = $event; $event = $eventName ?? new Event(); diff --git a/src/Symfony/Component/EventDispatcher/LegacyEventDispatcherProxy.php b/src/Symfony/Component/EventDispatcher/LegacyEventDispatcherProxy.php index cc1a4b93606a1..513e260e98106 100644 --- a/src/Symfony/Component/EventDispatcher/LegacyEventDispatcherProxy.php +++ b/src/Symfony/Component/EventDispatcher/LegacyEventDispatcherProxy.php @@ -59,7 +59,7 @@ public function dispatch($event/*, string $eventName = null*/) if (\is_object($event)) { $eventName = $eventName ?? \get_class($event); - } elseif (\is_string($event) && (null === $eventName || $eventName instanceof Event)) { + } elseif (\is_string($event) && (null === $eventName || $eventName instanceof ContractsEvent || $eventName instanceof Event)) { @trigger_error(sprintf('Calling the "%s::dispatch()" method with the event name as the first argument is deprecated since Symfony 4.3, pass it as the second argument and provide the event object as the first argument instead.', ContractsEventDispatcherInterface::class), E_USER_DEPRECATED); $swap = $event; $event = $eventName ?? new Event(); diff --git a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php index 215afcd5b82eb..7c8b5dc9b20fe 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/EventDispatcherTest.php @@ -440,10 +440,12 @@ public function testLegacySignatureWithEvent() $this->dispatcher->dispatch('foo', new Event()); } + /** + * @group legacy + * @expectedDeprecation Calling the "Symfony\Component\EventDispatcher\EventDispatcherInterface::dispatch()" method with the event name as the first argument is deprecated since Symfony 4.3, pass it as the second argument and provide the event object as the first argument instead. + */ public function testLegacySignatureWithNewEventObject() { - $this->expectException('TypeError'); - $this->expectExceptionMessage('Argument 1 passed to "Symfony\Component\EventDispatcher\EventDispatcherInterface::dispatch()" must be an object, string given.'); $this->dispatcher->dispatch('foo', new ContractsEvent()); } } diff --git a/src/Symfony/Component/EventDispatcher/Tests/LegacyEventDispatcherProxyTest.php b/src/Symfony/Component/EventDispatcher/Tests/LegacyEventDispatcherProxyTest.php index 3b7721d2a37c8..f0469f9538471 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/LegacyEventDispatcherProxyTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/LegacyEventDispatcherProxyTest.php @@ -41,10 +41,14 @@ public function testLegacySignatureWithEvent() $this->createEventDispatcher()->dispatch('foo', new Event()); } + /** + * @group legacy + * @expectedDeprecation The signature of the "Symfony\Component\EventDispatcher\Tests\TestLegacyEventDispatcher::dispatch()" method should be updated to "dispatch($event, string $eventName = null)", not doing so is deprecated since Symfony 4.3. + * @expectedDeprecation The signature of the "Symfony\Component\EventDispatcher\Tests\TestLegacyEventDispatcher::dispatch()" method should be updated to "dispatch($event, string $eventName = null)", not doing so is deprecated since Symfony 4.3. + * @expectedDeprecation Calling the "Symfony\Contracts\EventDispatcher\EventDispatcherInterface::dispatch()" method with the event name as the first argument is deprecated since Symfony 4.3, pass it as the second argument and provide the event object as the first argument instead. + */ public function testLegacySignatureWithNewEventObject() { - $this->expectException('TypeError'); - $this->expectExceptionMessage('Argument 1 passed to "Symfony\Contracts\EventDispatcher\EventDispatcherInterface::dispatch()" must be an object, string given.'); $this->createEventDispatcher()->dispatch('foo', new ContractsEvent()); } diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index 94e1024af504b..c85860be7f0f0 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -797,7 +797,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/ButtonBuilder.php b/src/Symfony/Component/Form/ButtonBuilder.php index b82314c0184cf..2af869c66d0d4 100644 --- a/src/Symfony/Component/Form/ButtonBuilder.php +++ b/src/Symfony/Component/Form/ButtonBuilder.php @@ -63,7 +63,7 @@ public function __construct(?string $name, array $options = []) if (preg_match('/^([^a-zA-Z0-9_].*)?(.*[^a-zA-Z0-9_\-:].*)?$/D', $name, $matches)) { if (isset($matches[1])) { - @trigger_error(sprintf('Using names for buttons that do not start with a lowercase letter, a digit, or an underscore is deprecated since Symfony 4.3 and will throw an exception in 5.0 ("%s" given).', $name), E_USER_DEPRECATED); + @trigger_error(sprintf('Using names for buttons that do not start with a letter, a digit, or an underscore is deprecated since Symfony 4.3 and will throw an exception in 5.0 ("%s" given).', $name), E_USER_DEPRECATED); } if (isset($matches[2])) { @trigger_error(sprintf('Using names for buttons that do not contain only letters, digits, underscores ("_"), hyphens ("-") and colons (":") ("%s" given) is deprecated since Symfony 4.3 and will throw an exception in 5.0.', $name), E_USER_DEPRECATED); diff --git a/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php b/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php index 7638c104d465a..e401351660c1f 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 b3b0959465cfe..96bbe695b92ea 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[] @@ -38,7 +39,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); @@ -53,6 +54,13 @@ public function collect(Request $request, Response $response/*, \Throwable $exce } } + public function lateCollect() + { + foreach ($this->clients as $client) { + $client->reset(); + } + } + public function getClients(): array { return $this->data['clients'] ?? []; @@ -68,17 +76,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} */ @@ -87,7 +84,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 e734b21554df6..68169d9da90fb 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -24,7 +24,8 @@ "php": "^7.1.3", "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,11 +34,7 @@ "psr/http-client": "^1.0", "symfony/dependency-injection": "^4.3|^5.0", "symfony/http-kernel": "^4.4", - "symfony/process": "^4.2|^5.0", - "symfony/service-contracts": "^1.0|^2" - }, - "conflict": { - "symfony/http-kernel": "<4.4" + "symfony/process": "^4.2|^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 f02075f63021c..64800b3fa4539 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -343,7 +343,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. * * @param bool $shouldDelete diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index e33b33ccbe4c7..3fa73a26a9074 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -7,6 +7,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 59758428c693b..7ffcdab41dca4 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/MemoryDataCollector.php @@ -93,7 +93,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 06218e002cbd8..716dcac09bca7 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, $catch = false) } // 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 d965080ae45e9..737e2777f936b 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -76,11 +76,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - const VERSION = '4.4.0'; - const VERSION_ID = 40400; + const VERSION = '4.4.1'; + const VERSION_ID = 40401; const MAJOR_VERSION = 4; const MINOR_VERSION = 4; - const RELEASE_VERSION = 0; + const RELEASE_VERSION = 1; const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '11/2022'; @@ -514,8 +514,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); 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 1f6811639b94c..deac82b72d5fc 100644 --- a/src/Symfony/Component/HttpKernel/Tests/KernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/KernelTest.php @@ -30,7 +30,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 dc7d3ae304aef..bb999bf201389 100644 --- a/src/Symfony/Component/Ldap/Ldap.php +++ b/src/Symfony/Component/Ldap/Ldap.php @@ -75,11 +75,7 @@ public function escape($subject, $ignore = '', $flags = 0): string public static function create($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/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 14be8444b2b74..c376903287544 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 c391ad6648aa8..16dd6c9a36fb2 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, $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/Loader/ObjectRouteLoader.php b/src/Symfony/Component/Routing/Loader/ObjectRouteLoader.php index 2bed560322145..39833d9ce6f93 100644 --- a/src/Symfony/Component/Routing/Loader/ObjectRouteLoader.php +++ b/src/Symfony/Component/Routing/Loader/ObjectRouteLoader.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Routing\Loader; -use Symfony\Component\Routing\RouteCollection; - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ObjectRouteLoader::class, ObjectLoader::class), E_USER_DEPRECATED); /** @@ -36,28 +34,6 @@ abstract class ObjectRouteLoader extends ObjectLoader */ abstract protected function getServiceObject($id); - /** - * Calls the service that will load the routes. - * - * @param string $resource Some value that will resolve to a callable - * @param string|null $type The resource type - * - * @return RouteCollection - */ - public function load($resource, $type = null) - { - if (!preg_match('/^[^\:]+(?:::?(?:[^\:]+))?$/', $resource)) { - throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the "service" route loader: use the format "service::method" or "service" if your service has an "__invoke" method.', $resource)); - } - - if (1 === substr_count($resource, ':')) { - $resource = str_replace(':', '::', $resource); - @trigger_error(sprintf('Referencing service route loaders with a single colon is deprecated since Symfony 4.1. Use %s instead.', $resource), E_USER_DEPRECATED); - } - - return parent::load($resource, $type); - } - /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index f471ce4bc9881..b44dfa1b62cfe 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -93,9 +93,7 @@ public function match($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 591dd15938695..1d68b17e39321 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/Routing/Tests/Loader/ObjectRouteLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/ObjectRouteLoaderTest.php index cc0aa5d50711e..3436d60693730 100644 --- a/src/Symfony/Component/Routing/Tests/Loader/ObjectRouteLoaderTest.php +++ b/src/Symfony/Component/Routing/Tests/Loader/ObjectRouteLoaderTest.php @@ -22,7 +22,7 @@ class ObjectRouteLoaderTest extends TestCase { /** - * @expectedDeprecation Referencing service route loaders with a single colon is deprecated since Symfony 4.1. Use my_route_provider_service::loadRoutes instead. + * @expectedDeprecation Referencing object route loaders with a single colon is deprecated since Symfony 4.1. Use my_route_provider_service::loadRoutes instead. */ public function testLoadCallsServiceAndReturnsCollectionWithLegacyNotation() { diff --git a/src/Symfony/Component/Security/CHANGELOG.md b/src/Symfony/Component/Security/CHANGELOG.md index cc30a5608fffa..0877880d880c1 100644 --- a/src/Symfony/Component/Security/CHANGELOG.md +++ b/src/Symfony/Component/Security/CHANGELOG.md @@ -14,6 +14,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 bcfa30dc585bd..83f0c5d2bc163 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\Firewall\LegacyListenerTrait; use Symfony\Component\Security\Http\Firewall\ListenerInterface; use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface; @@ -33,7 +34,7 @@ * * @final since Symfony 4.3 */ -class GuardAuthenticationListener implements ListenerInterface +class GuardAuthenticationListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -62,9 +63,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]; @@ -76,7 +77,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; @@ -97,19 +130,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 af3ce94a9b2d0..09b30d11ef3c9 100644 --- a/src/Symfony/Component/Security/Guard/composer.json +++ b/src/Symfony/Component/Security/Guard/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.1.3", "symfony/security-core": "^3.4.22|^4.2.3|^5.0", - "symfony/security-http": "^4.3" + "symfony/security-http": "^4.4.1" }, "require-dev": { "psr/log": "~1.0" diff --git a/src/Symfony/Component/Security/Http/Firewall.php b/src/Symfony/Component/Security/Http/Firewall.php index 08d4873c28af8..ee769496a6918 100644 --- a/src/Symfony/Component/Security/Http/Firewall.php +++ b/src/Symfony/Component/Security/Http/Firewall.php @@ -138,7 +138,7 @@ protected function handleRequest(GetResponseEvent $event, $listeners) if (\is_callable($listener)) { $listener($event); } else { - @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($listener)), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, extend "%s" instead.', \get_class($listener), AbstractListener::class), E_USER_DEPRECATED); $listener->handle($event); } diff --git a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php index fed7785a6153a..736c247e7d924 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractAuthenticationListener.php @@ -49,7 +49,7 @@ * @author Fabien Potencier * @author Johannes M. Schmitt */ -abstract class AbstractAuthenticationListener implements ListenerInterface +abstract class AbstractAuthenticationListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -105,20 +105,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 500ae43e498bd..e14dd1a95a946 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AbstractPreAuthenticatedListener.php @@ -35,7 +35,7 @@ * * @internal since Symfony 4.3 */ -abstract class AbstractPreAuthenticatedListener implements ListenerInterface +abstract class AbstractPreAuthenticatedListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -56,20 +56,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 6164adde5db02..00673f60aba2d 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 since Symfony 4.3 */ -class AccessListener implements ListenerInterface +class AccessListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -44,13 +46,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.'); @@ -58,9 +71,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 b7a7381bfc885..0f1da391e6dff 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 since Symfony 4.3 */ -class AnonymousAuthenticationListener implements ListenerInterface +class AnonymousAuthenticationListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -43,10 +44,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 9d6d81715c294..dd18e87c5b307 100644 --- a/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/BasicAuthenticationListener.php @@ -29,7 +29,7 @@ * * @final since Symfony 4.3 */ -class BasicAuthenticationListener implements ListenerInterface +class BasicAuthenticationListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -55,10 +55,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 671f279fdf9a2..1033aa47ed3b4 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 since Symfony 4.3 */ -class ChannelListener implements ListenerInterface +class ChannelListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -42,10 +43,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()) { @@ -59,11 +58,7 @@ public function __invoke(RequestEvent $event) } } - $response = $this->authenticationEntryPoint->start($request); - - $event->setResponse($response); - - return; + return true; } if ('http' === $channel && $request->isSecure()) { @@ -71,9 +66,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 4015262f01b87..e9e29ab46773f 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -32,6 +32,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; /** * ContextListener manages the SecurityContext persistence through a session. @@ -41,7 +42,7 @@ * * @final since Symfony 4.3 */ -class ContextListener implements ListenerInterface +class ContextListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -52,6 +53,7 @@ class ContextListener implements ListenerInterface private $dispatcher; private $registered; private $trustResolver; + private $rememberMeServices; private $sessionTrackerEnabler; /** @@ -84,10 +86,18 @@ public function setLogoutOnUserChange($logoutOnUserChange) @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1.', __METHOD__), E_USER_DEPRECATED); } + /** + * {@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']); @@ -128,6 +138,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]); @@ -307,4 +321,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 a53aeccf4a25a..e78f21826f362 100644 --- a/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/LogoutListener.php @@ -30,7 +30,7 @@ * * @final since Symfony 4.3 */ -class LogoutListener implements ListenerInterface +class LogoutListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -63,6 +63,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. * @@ -72,14 +80,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 ebc03db862952..0cfac54b3412d 100644 --- a/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/RememberMeListener.php @@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy; +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; @@ -31,7 +32,7 @@ * * @final since Symfony 4.3 */ -class RememberMeListener implements ListenerInterface +class RememberMeListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -54,10 +55,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/SimplePreAuthenticationListener.php b/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php index 2c444e823b6fe..0641d9e45a128 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php @@ -41,7 +41,7 @@ * * @deprecated since Symfony 4.2, use Guard instead. */ -class SimplePreAuthenticationListener implements ListenerInterface +class SimplePreAuthenticationListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -79,10 +79,28 @@ public function setSessionAuthenticationStrategy(SessionAuthenticationStrategyIn $this->sessionStrategy = $sessionStrategy; } + public function supports(Request $request): ?bool + { + if ((null !== $token = $this->tokenStorage->getToken()) && !$this->trustResolver->isAnonymous($token)) { + return false; + } + + $token = $this->simpleAuthenticator->createToken($request, $this->providerKey); + + // allow null to be returned to skip authentication + if (null === $token) { + return false; + } + + $request->attributes->set('_simple_pre_authenticator_token', $token); + + return true; + } + /** * Handles basic authentication. */ - public function __invoke(RequestEvent $event) + public function authenticate(RequestEvent $event) { $request = $event->getRequest(); @@ -91,16 +109,14 @@ public function __invoke(RequestEvent $event) } if ((null !== $token = $this->tokenStorage->getToken()) && !$this->trustResolver->isAnonymous($token)) { + $request->attributes->remove('_simple_pre_authenticator_token'); + return; } try { - $token = $this->simpleAuthenticator->createToken($request, $this->providerKey); - - // allow null to be returned to skip authentication - if (null === $token) { - return; - } + $token = $request->attributes->get('_simple_pre_authenticator_token'); + $request->attributes->remove('_simple_pre_authenticator_token'); $token = $this->authenticationManager->authenticate($token); diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php index 4d546285f52a2..2263623d75d1b 100644 --- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php @@ -39,7 +39,7 @@ * * @final since Symfony 4.3 */ -class SwitchUserListener implements ListenerInterface +class SwitchUserListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -75,14 +75,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); @@ -92,9 +88,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.'); } @@ -148,7 +161,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 851e160bebbef..50eb405c6120d 100644 --- a/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/UsernamePasswordJsonAuthenticationListener.php @@ -44,7 +44,7 @@ * * @final since Symfony 4.3 */ -class UsernamePasswordJsonAuthenticationListener implements ListenerInterface +class UsernamePasswordJsonAuthenticationListener extends AbstractListener implements ListenerInterface { use LegacyListenerTrait; @@ -74,22 +74,27 @@ public function __construct(TokenStorageInterface $tokenStorage, AuthenticationM $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } - /** - * {@inheritdoc} - */ - 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 4afdd1fe08978..9353d48ba652f 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'); @@ -351,7 +365,7 @@ protected function runSessionOnKernelResponse($newToken, $original = null) $request->cookies->set('MOCKSESSID', true); $sessionId = $session->getId(); - $usageIndex = \method_exists(Request::class, 'getPreferredFormat') ? $session->getUsageIndex() : null; + $usageIndex = method_exists(Request::class, 'getPreferredFormat') ? $session->getUsageIndex() : null; $event = new ResponseEvent( $this->getMockBuilder(HttpKernelInterface::class)->getMock(), @@ -374,7 +388,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()); @@ -388,7 +402,7 @@ private function handleEventWithPreviousSession($userProviders, UserInterface $u $usageIndex = null; $sessionTrackerEnabler = null; - if (\method_exists(Request::class, 'getPreferredFormat')) { + if (method_exists(Request::class, 'getPreferredFormat')) { $usageIndex = $session->getUsageIndex(); $tokenStorage = new UsageTrackingTokenStorage($tokenStorage, new class([ 'session' => function () use ($session) { return $session; } @@ -399,6 +413,10 @@ private function handleEventWithPreviousSession($userProviders, UserInterface $u } $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)); if (null !== $usageIndex) { 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 59fe793a3fe67..56d93d4408253 100644 --- a/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php +++ b/src/Symfony/Component/Serializer/Encoder/CsvEncoder.php @@ -44,6 +44,7 @@ class CsvEncoder implements EncoderInterface, DecoderInterface self::HEADERS_KEY => [], self::KEY_SEPARATOR_KEY => '.', self::NO_HEADERS_KEY => false, + self::AS_COLLECTION_KEY => false, self::OUTPUT_UTF8_BOM_KEY => false, ]; @@ -106,7 +107,7 @@ public function encode($data, $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); } @@ -164,7 +165,7 @@ public function decode($data, $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]; } @@ -204,7 +205,7 @@ public function decode($data, $format, array $context = []) } fclose($handle); - if ($context[self::AS_COLLECTION_KEY] ?? false) { + if ($context[self::AS_COLLECTION_KEY] ?? $this->defaultContext[self::AS_COLLECTION_KEY]) { return $result; } 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 edde9949176ef..bbf70eaac3734 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 0569c2923bcda..1b88957f76d74 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, $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 9518b1f86848d..33a16ee4402e2 100644 --- a/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php +++ b/src/Symfony/Component/Serializer/Tests/Encoder/CsvEncoderTest.php @@ -203,6 +203,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')); @@ -374,6 +392,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() @@ -562,6 +589,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 = [ @@ -591,6 +635,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 d95c3ca91d2ee..540e59ed58aff 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 569ccfa93d4be..671e97e9c3811 100644 --- a/src/Symfony/Component/Validator/Constraint.php +++ b/src/Symfony/Component/Validator/Constraint.php @@ -288,7 +288,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 1863641c91e58..e3be41abf4516 100644 --- a/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php @@ -179,7 +179,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) @@ -191,7 +192,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 a466f2d56626d..d1ee9879b76f5 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, $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 559136ff322f0..8c219f502bc3d 100644 --- a/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php +++ b/src/Symfony/Component/Workflow/MarkingStore/MethodMarkingStore.php @@ -51,7 +51,7 @@ public function getMarking($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($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 32cabdaae5b11..14e57bffdb154 100644 --- a/src/Symfony/Component/Workflow/Workflow.php +++ b/src/Symfony/Component/Workflow/Workflow.php @@ -159,25 +159,47 @@ public function apply($subject, $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); @@ -193,14 +215,6 @@ public function apply($subject, $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; } }