diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md index 7bd4ac031e861..8dd0f9779c1fc 100644 --- a/CHANGELOG-4.4.md +++ b/CHANGELOG-4.4.md @@ -7,6 +7,81 @@ 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.2 (2019-12-19) + + * bug #35051 [DependencyInjection] Fix binding tagged services to containers (nicolas-grekas) + * bug #35039 [DI] skip looking for config class when the extension class is anonymous (nicolas-grekas) + * bug #35049 [ProxyManager] fix generating proxies for root-namespaced classes (nicolas-grekas) + * bug #35022 [Dotenv] FIX missing getenv (mccullagh) + * bug #35023 [HttpKernel] ignore failures generated by opcache.restrict_api (nicolas-grekas) + * bug #35024 [HttpFoundation] fix pdo session handler for sqlsrv (azjezz) + * bug #35025 [HttpClient][Psr18Client] Remove Psr18ExceptionTrait (fancyweb) + * bug #35015 [Config] fix perf of glob discovery when GLOB_BRACE is not available (nicolas-grekas) + * bug #35014 [HttpClient] make pushed responses retry-able (nicolas-grekas) + * bug #35010 [VarDumper] ignore failing __debugInfo() (nicolas-grekas) + * bug #34998 [DI] fix auto-binding service providers to their service subscribers (nicolas-grekas) + * bug #34954 [Mailer] Fixed undefined index when sending via Mandrill API (wulff) + * bug #33670 [DI] Service locators can't be decorated (malarzm) + * bug #35000 [Console][SymfonyQuestionHelper] Handle multibytes question choices keys and custom prompt (fancyweb) + * bug #35005 [HttpClient] force HTTP/1.1 when NTLM auth is used (nicolas-grekas) + * bug #34707 [Validation][FrameworkBundle] Allow EnableAutoMapping to work without auto-mapping namespaces (ogizanagi) + * bug #34996 Fix displaying anonymous classes on PHP 7.4 (nicolas-grekas) + * bug #29839 [Validator] fix comparisons with null values at property paths (xabbuh) + * bug #34900 [DoctrineBridge] Fixed submitting invalid ids when using queries with limit (HeahDude) + * bug #34791 [Serializer] Skip uninitialized (PHP 7.4) properties in PropertyNormalizer and ObjectNormalizer (vudaltsov) + * bug #34956 [Messenger][AMQP] Use delivery_mode=2 by default (lyrixx) + * bug #34915 [FrameworkBundle] Fix invalid Windows path normalization in TemplateNameParser (mvorisek) + * bug #34981 stop using deprecated Doctrine persistence classes (xabbuh) + * bug #34904 [Validator][ConstraintValidator] Safe fail on invalid timezones (fancyweb) + * bug #34935 [FrameworkBundle][DependencyInjection] Skip removed ids in the lint container command and its associated pass (fancyweb) + * bug #34957 [Security] Revert "AbstractAuthenticationListener.php error instead info" (larzuk91) + * bug #34922 [FrameworkBundle][Secrets] Hook configured local dotenv file (fancyweb) + * bug #34967 [HttpFoundation] fix redis multi host dsn not recognized (Jan Christoph Beyer) + * bug #34963 [Lock] fix constructor argument type declaration (xabbuh) + * bug #34955 Require doctrine/persistence ^1.3 (nicolas-grekas) + * bug #34923 [DI] Fix support for immutable setters in CallTrait (Lctrs) + * bug #34878 [TwigBundle] fix broken FilesystemLoader::exists() with Twig 3 (dpesch) + * bug #34921 [HttpFoundation] Removed "Content-Type" from the preferred format guessing mechanism (yceruto) + * bug #34886 [HttpKernel] fix triggering deprecation in file locator (xabbuh) + * bug #34918 [Translation] fix memoryleak in PhpFileLoader (nicolas-grekas) + * bug #34920 [Routing] fix memoryleak when loading compiled routes (nicolas-grekas) + * bug #34787 [Cache] Propagate expiry when syncing items in ChainAdapter (trvrnrth) + * bug #34694 [Validator] Fix auto-mapping constraints should not be validated (ogizanagi) + * bug #34848 [Process] change the syntax of portable command lines (nicolas-grekas) + * bug #34862 [FrameworkBundle][ContainerLintCommand] Reinitialize bundles when the container is reprepared (fancyweb) + * bug #34896 [Cache] fix memory leak when using PhpFilesAdapter (nicolas-grekas) + * bug #34438 [HttpFoundation] Use `Cache-Control: must-revalidate` only if explicit lifetime has been given (mpdude) + * bug #34449 [Yaml] Implement multiline string as scalar block for tagged values (natepage) + * bug #34601 [MonologBridge] Fix debug processor datetime type (mRoca) + * bug #34842 [ExpressionLanguage] Process division by zero (tigr1991) + * bug #34902 [PropertyAccess] forward caught exception (xabbuh) + * bug #34903 Fixing bad order of operations with null coalescing operator (weaverryan) + * bug #34888 [TwigBundle] add tags before processing them (xabbuh) + * bug #34760 [Mailer] Fix SMTP Authentication when using STARTTLS (DjLeChuck) + * bug #34762 [Config] never try loading failed classes twice with ClassExistenceResource (nicolas-grekas) + * bug #34783 [DependencyInjection] Handle env var placeholders in CheckTypeDeclarationsPass (fancyweb) + * bug #34839 [Cache] fix memory leak when using PhpArrayAdapter (nicolas-grekas) + * bug #34812 [Yaml] fix parsing negative octal numbers (xabbuh) + * bug #34854 [Messenger] gracefully handle missing event dispatchers (xabbuh) + * bug #34802 [Security] Check UserInterface::getPassword is not null before calling needsRehash (dbrekelmans) + * bug #34788 [SecurityBundle] Properly escape regex in AddSessionDomainConstraintPass (fancyweb) + * bug #34859 [SecurityBundle] Fix TokenStorage::reset not called in stateless firewall (jderusse) + * bug #34827 [HttpFoundation] get currently session.gc_maxlifetime if ttl doesnt exists (rafaeltovar) + * bug #34755 [FrameworkBundle] resolve service locators in `debug:*` commands (nicolas-grekas) + * bug #34832 [Validator] Allow underscore character "_" in URL username and password (romainneutron) + * bug #34811 [TwigBridge] Update bootstrap_4_layout.html.twig missing switch-custom label (sabruss) + * bug #34820 [FrameworkBundle][SodiumVault] Create secrets directory only when it is used (fancyweb) + * bug #34776 [DI] fix resolving bindings for named TypedReference (nicolas-grekas) + * bug #34794 [DependencyInjection] Resolve expressions in CheckTypeDeclarationsPass (fancyweb) + * bug #34797 [Translation] Fix FileDumper behavior (yceruto) + * bug #34738 [SecurityBundle] Passwords are not encoded when algorithm set to "true" (nieuwenhuisen) + * bug #34759 [SecurityBundle] Fix switch_user provider configuration handling (fancyweb) + * bug #34779 [Security] do not validate passwords when the hash is null (xabbuh) + * bug #34786 [SecurityBundle] Use config variable in AnonymousFactory (martijnboers) + * bug #34784 [FrameworkBundle] Set the parameter bag as resolved in ContainerLintCommand (fancyweb) + * bug #34763 [Security/Core] Fix checking for SHA256/SHA512 passwords (David Brooks) + * bug #34757 [DI] Fix making the container path-independent when the app is in /app (nicolas-grekas) + * 4.4.1 (2019-12-01) * bug #34732 [DependencyInjection][Xml] Fix the attribute 'tag' is not allowed in 'bind' tag (tienvx) diff --git a/CHANGELOG-5.0.md b/CHANGELOG-5.0.md index 438bdece7ac9d..a7af44177f3c7 100644 --- a/CHANGELOG-5.0.md +++ b/CHANGELOG-5.0.md @@ -7,6 +7,70 @@ in 5.0 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v5.0.0...v5.0.1 +* 5.0.3 (2020-01-21) + + * bug #35364 [Yaml] Throw on unquoted exclamation mark (fancyweb) + * bug #35065 [Security] Use supportsClass in addition to UnsupportedUserException (linaori) + * bug #35351 Revert #34797 "Fixed translations file dumper behavior" and fix #34713 (yceruto) + * bug #35356 [Filesystem] chown and chgrp should also accept int as owner and group (Slamdunk) + * bug #35335 [Security] Fix RememberMe with null password (jderusse) + * bug #35339 [String] add missing encoding when calling mb_ord() (nicolas-grekas) + * bug #35355 [DI] Fix EnvVar not loaded when Loader requires an env var (jderusse) + * bug #35343 [Security] Fix RememberMe with null password (jderusse) + * bug #34223 [DI] Suggest typed argument when binding fails with untyped argument (gudfar) + * bug #35323 [FrameworkBundle] Set booted flag to false when test kernel is unset (thiagocordeiro) + * bug #35324 [HttpClient] Fix strict parsing of response status codes (Armando-Walmeric) + * bug #35318 [Yaml] fix PHP const mapping keys using the inline notation (xabbuh) + * bug #35306 [FrameworkBundle] Make sure one can use fragments.hinclude_default_template (Nyholm) + * bug #35304 [HttpKernel] Fix that no-cache MUST revalidate with the origin (mpdude) + * bug #35299 Avoid `stale-if-error` in FrameworkBundle's HttpCache if kernel.debug = true (mpdude) + * bug #35240 [SecurityBundle] Fix collecting traceable listeners info on lazy firewalls (chalasr) + * bug #35151 [DI] deferred exceptions in ResolveParameterPlaceHoldersPass (Islam93) + * bug #35290 [Filesystem][FilesystemCommonTrait] Use a dedicated directory when there are no namespace (fancyweb) + * bug #35099 [FrameworkBundle] Do not throw exception on value generate key (jderusse) + * bug #35278 [EventDispatcher] expand listener in place (xabbuh) + * bug #35269 [HttpKernel][FileLocator] Fix deprecation message (fancyweb) + * bug #35254 [PHPUnit-Bridge] Fail-fast in simple-phpunit if one of the passthru() commands fails (mpdude) + * bug #35261 [Routing] Fix using a custom matcher & generator dumper class (fancyweb) + * bug #34643 [Dotenv] Fixed infinite loop with missing quote followed by quoted value (naitsirch) + * bug #35239 [Security\Http] Prevent canceled remember-me cookie from being accepted (chalasr) + * bug #35267 [Debug] fix ClassNotFoundFatalErrorHandler (nicolas-grekas) + * bug #35252 [Serializer] Fix cache in MetadataAwareNameConverter (bastnic) + * bug #35200 [TwigBridge] do not render preferred choices as selected (xabbuh) + * bug #35243 [HttpKernel] release lock explicitly (nicolas-grekas) + * bug #35193 [TwigBridge] button_widget now has its title attr translated even if its label = null or false (stephen-lewis) + * bug #35219 [PhpUnitBridge] When using phpenv + phpenv-composer plugin, composer executable is wrapped into a bash script (oleg-andreyev) + * bug #35150 [Messenger] Added check if json_encode succeeded (toooni) + * bug #35137 [Messenger] Added check if json_encode succeeded (toooni) + * bug #35170 [FrameworkBundle][TranslationUpdateCommand] Do not output positive feedback on stderr (fancyweb) + * bug #35245 [HttpClient] fix exception in case of PSR17 discovery failure (nicolas-grekas) + * bug #35244 [Cache] fix processing chain adapter based cache pool (xabbuh) + * bug #35247 [FrameworkBundle][ContainerLintCommand] Only skip .errored. services (fancyweb) + * bug #35225 [DependencyInjection] Handle ServiceClosureArgument for callable in container linting (shieldo) + * bug #35223 [HttpClient] Don't read from the network faster than the CPU can deal with (nicolas-grekas) + * bug #35214 [DI] DecoratorServicePass should keep container.service_locator on the decorated definition (malarzm) + * bug #35209 [HttpClient] fix support for non-blocking resource streams (nicolas-grekas) + * bug #35210 [HttpClient] NativeHttpClient should not send >1.1 protocol version (nicolas-grekas) + * bug #35162 [Mailer] Make sure you can pass custom headers to Mailgun (Nyholm) + * bug #33672 [Mailer] Remove line breaks in email attachment content (Stuart Fyfe) + * bug #35101 [Routing] Fix i18n routing when the url contains the locale (fancyweb) + * bug #35124 [TwigBridge][Form] Added missing help messages in form themes (cmen) + * bug #35195 [HttpClient] fix casting responses to PHP streams (nicolas-grekas) + * bug #35168 [HttpClient] fix capturing SSL certificates with NativeHttpClient (nicolas-grekas) + * bug #35134 [PropertyInfo] Fix BC issue in phpDoc Reflection library (jaapio) + * bug #35184 [Mailer] Payload sent to Sendgrid doesn't include names (versgui) + * bug #35173 [Mailer][MailchimpBridge] Fix missing attachments when sending via Mandrill API (vilius-g) + * bug #35172 [Mailer][MailchimpBridge] Fix incorrect sender address when sender has name (vilius-g) + * bug #35125 [Translator] fix performance issue in MessageCatalogue and catalogue operations (ArtemBrovko) + * bug #35120 [HttpClient] fix scheduling pending NativeResponse (nicolas-grekas) + * bug #35117 [Cache] do not overwrite variable value (xabbuh) + * bug #35113 [VarDumper] Fix "Undefined index: argv" when using CliContextProvider (xepozz) + * bug #34673 Migrate server:log command away from WebServerBundle (jderusse) + * bug #35103 [Translation] Use `locale_parse` for computing fallback locales (alanpoulain) + * bug #35060 [Security] Fix missing defaults for auto-migrating encoders (chalasr) + * bug #35067 [DependencyInjection][CheckTypeDeclarationsPass] Handle \Closure for callable (fancyweb) + * bug #35094 [Console] Fix filtering out identical alternatives when there is a command loader (fancyweb) + * 5.0.2 (2019-12-19) * bug #35051 [DependencyInjection] Fix binding tagged services to containers (nicolas-grekas) diff --git a/LICENSE b/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index c1bb03f5aebcb..8f7cc54411aa9 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -192,15 +192,15 @@ HttpKernel * The `Kernel::getRootDir()` and the `kernel.root_dir` parameter have been deprecated * The `KernelInterface::getName()` and the `kernel.name` parameter have been deprecated - * Deprecated the first and second constructor argument of `ConfigDataCollector` - * Deprecated `ConfigDataCollector::getApplicationName()` + * Deprecated the first and second constructor argument of `ConfigDataCollector` + * Deprecated `ConfigDataCollector::getApplicationName()` * Deprecated `ConfigDataCollector::getApplicationVersion()` Messenger --------- * The `MiddlewareInterface::handle()` and `SenderInterface::send()` methods must now return an `Envelope` instance. - * The return value of handlers isn't forwarded anymore by middleware and buses. + * The return value of handlers isn't forwarded anymore by middleware and buses. If you used to return a value, e.g in query bus handlers, you can either: - get the result from the `HandledStamp` in the envelope returned by the bus. - use the `HandleTrait` to leverage a message bus, expecting a single, synchronous message handling and returning its result. @@ -214,15 +214,15 @@ Messenger $query->setResult($yourResult); ``` * The `EnvelopeAwareInterface` was removed and the `MiddlewareInterface::handle()` method now requires an `Envelope` object - as first argument. When using built-in middleware with the provided `MessageBus`, you will not have to do anything. - If you use your own `MessageBusInterface` implementation, you must wrap the message in an `Envelope` before passing it to middleware. + as first argument. When using built-in middleware with the provided `MessageBus`, you will not have to do anything. + If you use your own `MessageBusInterface` implementation, you must wrap the message in an `Envelope` before passing it to middleware. If you created your own middleware, you must change the signature to always expect an `Envelope`. * The `MiddlewareInterface::handle()` second argument (`callable $next`) has changed in favor of a `StackInterface` instance. - When using built-in middleware with the provided `MessageBus`, you will not have to do anything. - If you use your own `MessageBusInterface` implementation, you can use the `StackMiddleware` implementation. + When using built-in middleware with the provided `MessageBus`, you will not have to do anything. + If you use your own `MessageBusInterface` implementation, you can use the `StackMiddleware` implementation. If you created your own middleware, you must change the signature to always expect an `StackInterface` instance and call `$stack->next()->handle($envelope, $stack)` instead of `$next` to call the next middleware: - + Before: ```php public function handle($message, callable $next): Envelope @@ -230,7 +230,7 @@ Messenger // do something before $message = $next($message); // do something after - + return $message; } ``` @@ -242,7 +242,7 @@ Messenger // do something before $envelope = $stack->next()->handle($envelope, $stack); // do something after - + return $envelope; } ``` @@ -251,7 +251,7 @@ Messenger respectively `ReceivedStamp`, `ValidationStamp`, `SerializerStamp` and moved to the `Stamp` namespace. * `AllowNoHandlerMiddleware` has been removed in favor of a new constructor argument on `HandleMessageMiddleware` * The `ConsumeMessagesCommand` class now takes an instance of `Psr\Container\ContainerInterface` - as first constructor argument, i.e a message bus locator. The CLI command now expects a mandatory + as first constructor argument, i.e a message bus locator. The CLI command now expects a mandatory `--bus` option value if there is more than one bus in the locator. * `MessageSubscriberInterface::getHandledMessages()` return value has changed. The value of an array item needs to be an associative array or the method name. diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 3409bea3b5b66..bcdee9ca1311f 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -306,7 +306,7 @@ TwigBundle The new default exception controller will also change the error response content according to https://tools.ietf.org/html/rfc7807 for `json`, `xml`, `atom` and `txt` formats: - Before: + Before (HTTP status code `200`): ```json { "error": { @@ -316,7 +316,7 @@ TwigBundle } ``` - After: + After (HTTP status code `404`): ```json { "title": "Not Found", diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 7e75c9593fbbc..fb773cc1b8e72 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -243,6 +243,7 @@ FrameworkBundle * Removed `routing.loader.service`. * Added support for PHPUnit 8. A `void` return-type was added to the `KernelTestCase::tearDown()` and `WebTestCase::tearDown()` method. * Removed the `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract` services. + * Removed the `router.cache_class_prefix` parameter. HttpClient ---------- @@ -392,7 +393,7 @@ Routing ------- * The `generator_base_class`, `generator_cache_class`, `matcher_base_class`, and `matcher_cache_class` router - options have been removed. + options have been removed. If you are using multiple Router instances and need separate caches for them, set a unique `cache_dir` per Router instance instead. * `Serializable` implementing methods for `Route` and `CompiledRoute` are final. Instead of overwriting them, use `__serialize` and `__unserialize` as extension points which are forward compatible with the new serialization methods in PHP 7.4. @@ -601,7 +602,7 @@ Workflow * `ClassInstanceSupportStrategy` has been removed, use `InstanceOfSupportStrategy` instead. * `WorkflowInterface::apply()` has a third argument: `array $context = []`. * `MarkingStoreInterface::setMarking()` has a third argument: `array $context = []`. - * Removed support of `initial_place`. Use `initial_places` instead. + * Removed support of `initial_place`. Use `initial_marking` instead. * `MultipleStateMarkingStore` has been removed. Use `MethodMarkingStore` instead. * `DefinitionBuilder::setInitialPlace()` has been removed, use `DefinitionBuilder::setInitialPlaces()` instead. diff --git a/composer.json b/composer.json index f7e384c6196f4..35d8d7361115a 100644 --- a/composer.json +++ b/composer.json @@ -107,7 +107,7 @@ "doctrine/dbal": "~2.4", "doctrine/orm": "~2.4,>=2.4.5", "doctrine/reflection": "~1.0", - "doctrine/doctrine-bundle": "^1.5|^2.0", + "doctrine/doctrine-bundle": "^2.0", "guzzlehttp/promises": "^1.3.1", "masterminds/html5": "^2.6", "monolog/monolog": "^1.25.1|^2", diff --git a/src/Symfony/Bridge/Doctrine/LICENSE b/src/Symfony/Bridge/Doctrine/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Bridge/Doctrine/LICENSE +++ b/src/Symfony/Bridge/Doctrine/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index 7fdb07c47e5a5..de5980cddaf0b 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG * The `RouteProcessor` class has been made final * Added `ElasticsearchLogstashHandler` +* Added the `ServerLogCommand`. Backport from the deprecated WebServerBundle 4.3.0 ----- diff --git a/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php new file mode 100644 index 0000000000000..60e7341db9e2c --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Command; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; +use Symfony\Bridge\Monolog\Handler\ConsoleHandler; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + +/** + * @author Grégoire Pineau + */ +class ServerLogCommand extends Command +{ + private static $bgColor = ['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow']; + + private $el; + private $handler; + + protected static $defaultName = 'server:log'; + + public function isEnabled() + { + if (!class_exists(ConsoleFormatter::class)) { + return false; + } + + // based on a symfony/symfony package, it crashes due a missing FormatterInterface from monolog/monolog + if (!interface_exists(FormatterInterface::class)) { + return false; + } + + return parent::isEnabled(); + } + + protected function configure() + { + if (!class_exists(ConsoleFormatter::class)) { + return; + } + + $this + ->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '0.0.0.0:9911') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT) + ->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE) + ->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"') + ->setDescription('Starts a log server that displays logs in real time') + ->setHelp(<<<'EOF' +%command.name% starts a log server to display in real time the log +messages generated by your application: + + php %command.full_name% + +To get the information as a machine readable format, use the +--filter option: + +php %command.full_name% --filter=port +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $filter = $input->getOption('filter'); + if ($filter) { + if (!class_exists(ExpressionLanguage::class)) { + throw new LogicException('Package "symfony/expression-language" is required to use the "filter" option.'); + } + $this->el = new ExpressionLanguage(); + } + + $this->handler = new ConsoleHandler($output, true, [ + OutputInterface::VERBOSITY_NORMAL => Logger::DEBUG, + ]); + + $this->handler->setFormatter(new ConsoleFormatter([ + 'format' => str_replace('\n', "\n", $input->getOption('format')), + 'date_format' => $input->getOption('date-format'), + 'colors' => $output->isDecorated(), + 'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(), + ])); + + if (false === strpos($host = $input->getOption('host'), '://')) { + $host = 'tcp://'.$host; + } + + if (!$socket = stream_socket_server($host, $errno, $errstr)) { + throw new RuntimeException(sprintf('Server start failed on "%s": %s %s.', $host, $errstr, $errno)); + } + + foreach ($this->getLogs($socket) as $clientId => $message) { + $record = unserialize(base64_decode($message)); + + // Impossible to decode the message, give up. + if (false === $record) { + continue; + } + + if ($filter && !$this->el->evaluate($filter, $record)) { + continue; + } + + $this->displayLog($output, $clientId, $record); + } + + return 0; + } + + private function getLogs($socket): iterable + { + $sockets = [(int) $socket => $socket]; + $write = []; + + while (true) { + $read = $sockets; + stream_select($read, $write, $write, null); + + foreach ($read as $stream) { + if ($socket === $stream) { + $stream = stream_socket_accept($socket); + $sockets[(int) $stream] = $stream; + } elseif (feof($stream)) { + unset($sockets[(int) $stream]); + fclose($stream); + } else { + yield (int) $stream => fgets($stream); + } + } + } + } + + private function displayLog(OutputInterface $output, int $clientId, array $record) + { + if (isset($record['log_id'])) { + $clientId = unpack('H*', $record['log_id'])[1]; + } + $logBlock = sprintf(' ', self::$bgColor[$clientId % 8]); + $output->write($logBlock); + + $this->handler->handle($record); + } +} diff --git a/src/Symfony/Bridge/Monolog/LICENSE b/src/Symfony/Bridge/Monolog/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Bridge/Monolog/LICENSE +++ b/src/Symfony/Bridge/Monolog/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bridge/PhpUnit/LICENSE b/src/Symfony/Bridge/PhpUnit/LICENSE index cf8b3ebe87145..684fbf94df83c 100644 --- a/src/Symfony/Bridge/PhpUnit/LICENSE +++ b/src/Symfony/Bridge/PhpUnit/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2019 Fabien Potencier +Copyright (c) 2014-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php index 2da40f2c204f1..a760d126ca854 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php @@ -47,11 +47,6 @@ public function startTest(\PHPUnit_Framework_Test $test) $this->trait->startTest($test); } - public function addWarning(\PHPUnit_Framework_Test $test, \PHPUnit_Framework_Warning $e, $time) - { - $this->trait->addWarning($test, $e, $time); - } - public function endTest(\PHPUnit_Framework_Test $test, $time) { $this->trait->endTest($test, $time); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php index 5b864bfe58a32..bcab4be4eeb25 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php @@ -52,11 +52,6 @@ public function startTest(Test $test) $this->trait->startTest($test); } - public function addWarning(Test $test, Warning $e, $time) - { - $this->trait->addWarning($test, $e, $time); - } - public function endTest(Test $test, $time) { $this->trait->endTest($test, $time); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV7.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV7.php index 18bbdbeba0f03..0c2069c66bf5e 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV7.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV7.php @@ -55,11 +55,6 @@ public function startTest(Test $test): void $this->trait->startTest($test); } - public function addWarning(Test $test, Warning $e, float $time): void - { - $this->trait->addWarning($test, $e, $time); - } - public function endTest(Test $test, float $time): void { $this->trait->endTest($test, $time); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php index 27a0d32654a72..c5dcb2b0433a3 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php @@ -40,8 +40,6 @@ class SymfonyTestsListenerTrait private $expectedDeprecations = array(); private $gatheredDeprecations = array(); private $previousErrorHandler; - private $testsWithWarnings; - private $reportUselessTests; private $error; private $runsInSeparateProcess = false; @@ -112,7 +110,6 @@ public function globalListenerDisabled() public function startTestSuite($suite) { $suiteName = $suite->getName(); - $this->testsWithWarnings = array(); foreach ($suite->tests() as $test) { if (!($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) { @@ -196,10 +193,6 @@ public function addSkippedTest($test, \Exception $e, $time) public function startTest($test) { if (-2 < $this->state && ($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) { - if (null !== $test->getTestResultObject()) { - $this->reportUselessTests = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything(); - } - // This event is triggered before the test is re-run in isolation if ($this->willBeIsolated($test)) { $this->runsInSeparateProcess = tempnam(sys_get_temp_dir(), 'deprec'); @@ -236,13 +229,6 @@ public function startTest($test) } } - public function addWarning($test, $e, $time) - { - if ($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase) { - $this->testsWithWarnings[$test->getName()] = true; - } - } - public function endTest($test, $time) { if (class_exists(DebugClassLoader::class, false)) { @@ -252,11 +238,6 @@ public function endTest($test, $time) $className = \get_class($test); $groups = Test::getGroups($className, $test->getName(false)); - if (null !== $this->reportUselessTests) { - $test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything($this->reportUselessTests); - $this->reportUselessTests = null; - } - if ($errored = null !== $this->error) { $test->getTestResultObject()->addError($test, $this->error, 0); $this->error = null; diff --git a/src/Symfony/Bridge/PhpUnit/Tests/ExpectedDeprecationAnnotationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/ExpectedDeprecationAnnotationTest.php new file mode 100644 index 0000000000000..259c99162a831 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/ExpectedDeprecationAnnotationTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests; + +use PHPUnit\Framework\TestCase; + +final class ExpectedDeprecationAnnotationTest extends TestCase +{ + /** + * Do not remove this test in the next major versions. + * + * @group legacy + * + * @expectedDeprecation foo + */ + public function testOne() + { + @trigger_error('foo', E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major versions. + * + * @group legacy + * + * @expectedDeprecation foo + * @expectedDeprecation bar + */ + public function testMany() + { + @trigger_error('foo', E_USER_DEPRECATED); + @trigger_error('bar', E_USER_DEPRECATED); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index 8ac4a79beb804..3c98499fb42f9 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -53,6 +53,14 @@ return $default; }; +$passthruOrFail = function ($command) { + passthru($command, $status); + + if ($status) { + exit($status); + } +}; + if (PHP_VERSION_ID >= 70200) { // PHPUnit 8 requires PHP 7.2+ $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '8.3'); @@ -63,9 +71,10 @@ // PHPUnit 6 requires PHP 7.0+ $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '6.5'); } elseif (PHP_VERSION_ID >= 50600) { - // PHPUnit 5 requires PHP 5.6+ + // PHPUnit 4 does not support PHP 7 $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '5.7'); } else { + // PHPUnit 5.1 requires PHP 5.6+ $PHPUNIT_VERSION = '4.8'; } @@ -106,7 +115,7 @@ || ($COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar 2> /dev/null`)) || ($COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer`) : `which composer 2> /dev/null`)) || file_exists($COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? `git rev-parse --show-toplevel 2> NUL` : `git rev-parse --show-toplevel 2> /dev/null`).DIRECTORY_SEPARATOR.'composer.phar') - ? $PHP.' '.escapeshellarg($COMPOSER) + ? (file_get_contents($COMPOSER, false, null, 0, 18) === '#!/usr/bin/env php' ? $PHP : '').' '.escapeshellarg($COMPOSER) // detect shell wrappers by looking at the shebang : 'composer'; $SYMFONY_PHPUNIT_REMOVE = $getEnvVar('SYMFONY_PHPUNIT_REMOVE', 'phpspec/prophecy'.($PHPUNIT_VERSION < 6.0 ? ' symfony/yaml': '')); @@ -122,25 +131,25 @@ rename("$PHPUNIT_VERSION_DIR", "$PHPUNIT_VERSION_DIR.old"); passthru(sprintf('\\' === DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s': 'rm -rf %s', "$PHPUNIT_VERSION_DIR.old")); } - passthru("$COMPOSER create-project --no-install --prefer-dist --no-scripts --no-plugins --no-progress --ansi phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\""); + $passthruOrFail("$COMPOSER create-project --no-install --prefer-dist --no-scripts --no-plugins --no-progress --ansi phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\""); @copy("$PHPUNIT_VERSION_DIR/phpunit.xsd", 'phpunit.xsd'); chdir("$PHPUNIT_VERSION_DIR"); if ($SYMFONY_PHPUNIT_REMOVE) { - passthru("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE); + $passthruOrFail("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE); } if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) { - passthru("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\""); + $passthruOrFail("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\""); } - passthru("$COMPOSER config --unset platform"); + $passthruOrFail("$COMPOSER config --unset platform.php"); if (file_exists($path = $root.'/vendor/symfony/phpunit-bridge')) { - passthru("$COMPOSER require --no-update symfony/phpunit-bridge \"*@dev\""); - passthru("$COMPOSER config repositories.phpunit-bridge path ".escapeshellarg(str_replace('/', DIRECTORY_SEPARATOR, $path))); + $passthruOrFail("$COMPOSER require --no-update symfony/phpunit-bridge \"*@dev\""); + $passthruOrFail("$COMPOSER config repositories.phpunit-bridge path ".escapeshellarg(str_replace('/', DIRECTORY_SEPARATOR, $path))); if ('\\' === DIRECTORY_SEPARATOR) { file_put_contents('composer.json', preg_replace('/^( {8})"phpunit-bridge": \{$/m', "$0\n$1 ".'"options": {"symlink": false},', file_get_contents('composer.json'))); } } else { - passthru("$COMPOSER require --no-update symfony/phpunit-bridge \"*\""); + $passthruOrFail("$COMPOSER require --no-update symfony/phpunit-bridge \"*\""); } $prevRoot = getenv('COMPOSER_ROOT_VERSION'); putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99"); @@ -206,7 +215,7 @@ class SymfonyBlacklistPhpunit {} $argv[1] = 'src/Symfony'; } if (isset($argv[1]) && is_dir($argv[1]) && !file_exists($argv[1].'/phpunit.xml.dist')) { - // Find Symfony components in plain PHP for Windows portability + // Find Symfony components in plain php for Windows portability $finder = new RecursiveDirectoryIterator($argv[1], FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::UNIX_PATHS); $finder = new RecursiveIteratorIterator($finder); diff --git a/src/Symfony/Bridge/ProxyManager/LICENSE b/src/Symfony/Bridge/ProxyManager/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Bridge/ProxyManager/LICENSE +++ b/src/Symfony/Bridge/ProxyManager/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bridge/Twig/LICENSE b/src/Symfony/Bridge/Twig/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Bridge/Twig/LICENSE +++ b/src/Symfony/Bridge/Twig/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal 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 4dc3e9c896fb7..49cd804398b5e 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 @@ -64,6 +64,7 @@ col-sm-10
{#--#}
{{- form_widget(form) -}} + {{- form_help(form) -}} {{- form_errors(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 f0cf0ad81854d..44492cebe74d7 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 @@ -148,6 +148,7 @@ {% block checkbox_row -%} {{- form_widget(form) -}} + {{- form_help(form) -}} {{- form_errors(form) -}} {%- endblock checkbox_row %} @@ -155,6 +156,7 @@ {% block radio_row -%} {{- form_widget(form) -}} + {{- form_help(form) -}} {{- form_errors(form) -}} {%- endblock radio_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 a97a38e5204e3..45843e713fd3d 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 @@ -65,12 +65,14 @@ {%- endif -%} {%- if preferred_choices|length > 0 -%} {% set options = preferred_choices %} + {% set render_preferred_choices = true %} {{- block('choice_widget_options') -}} {%- if choices|length > 0 and separator is not none -%} {%- endif -%} {%- endif -%} {%- set options = choices -%} + {%- set render_preferred_choices = false -%} {{- block('choice_widget_options') -}} {%- endblock choice_widget_collapsed -%} @@ -83,7 +85,7 @@ {{- block('choice_widget_options') -}} {%- else -%} - + {%- endif -%} {% endfor %} {%- endblock choice_widget_options -%} @@ -226,13 +228,11 @@ '%name%': name, '%id%': id, }) %} - {%- elseif label is same as(false) -%} - {% set translation_domain = false %} - {%- else -%} + {%- elseif label is not same as(false) -%} {% set label = name|humanize %} {%- endif -%} {%- endif -%} - + {%- endblock button_widget -%} {%- block submit_widget -%} 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 83c5e30d803bc..b02b94210d9be 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 @@ -160,12 +160,14 @@ {%- endif %} {%- if preferred_choices|length > 0 -%} {% set options = preferred_choices %} + {% set render_preferred_choices = true %} {{- block('choice_widget_options') -}} {% if choices|length > 0 and separator is not none -%} {%- endif %} {%- endif -%} {% set options = choices -%} + {%- set render_preferred_choices = false -%} {{- block('choice_widget_options') -}} {%- endblock choice_widget_collapsed %} @@ -311,6 +313,7 @@
{{ form_widget(form) }} + {{- form_help(form) -}} {{ form_errors(form) }}
@@ -320,6 +323,7 @@
{{ form_widget(form) }} + {{- form_help(form) -}} {{ form_errors(form) }}
diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php index 9131216182a3d..69064a003d7fe 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php @@ -163,4 +163,23 @@ public function testCheckboxRow() $this->assertMatchesXpath($html, '/div[@class="form-group"]/div[@class="col-sm-2" or @class="col-sm-10"]', 2); } + + public function testCheckboxRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType'); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, +'/div + [@class="form-group"] + [ + ./div[@class="col-sm-2" or @class="col-sm-10"] + /following-sibling::div[@class="col-sm-2" or @class="col-sm-10"] + [ + ./span[text() = "[trans]really helpful text[/trans]"] + ] + ] +' + ); + } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php index 8f38c8486ffda..8504f8b7c08b4 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php @@ -333,6 +333,21 @@ public function testCheckboxWithValue() ); } + public function testCheckboxRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType'); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, +'/div + [@class="form-group"] + [ + ./span[text() = "[trans]really helpful text[/trans]"] + ] +' + ); + } + public function testSingleChoice() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ @@ -531,6 +546,31 @@ public function testSingleChoiceWithPreferred() ); } + public function testSingleChoiceWithSelectedPreferred() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'preferred_choices' => ['&a'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --', 'attr' => ['class' => 'my&class']], +'/select + [@name="name"] + [@class="my&class form-control"] + [not(@required)] + [ + ./option[@value="&a"][not(@selected)][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=4] +' + ); + } + public function testSingleChoiceWithPreferredAndNoSeparator() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ @@ -2265,6 +2305,21 @@ public function testRadioWithValue() ); } + public function testRadioRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, +'/div + [@class="form-group"] + [ + ./span[text() = "[trans]really helpful text[/trans]"] + ] +' + ); + } + public function testRange() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RangeType', 42, ['attr' => ['min' => 5]]); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php index e9416b02213f2..e20f4818b2810 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php @@ -231,6 +231,25 @@ public function testCheckboxRowWithHelp() ./small[text() = "[trans]really helpful text[/trans]"] ] ] +' + ); + } + + public function testRadioRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, +'/div + [@class="form-group row"] + [ + ./div[@class="col-sm-2" or @class="col-sm-10"] + /following-sibling::div[@class="col-sm-2" or @class="col-sm-10"] + [ + ./small[text() = "[trans]really helpful text[/trans]"] + ] + ] ' ); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php index 89fbacf2fc2d7..2643274d47c3e 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php @@ -422,6 +422,21 @@ public function testCheckboxWithValue() ); } + public function testCheckboxRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType'); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, + '/div + [@class="form-group"] + [ + ./small[text() = "[trans]really helpful text[/trans]"] + ] +' + ); + } + public function testSingleChoiceExpanded() { $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ @@ -1027,6 +1042,21 @@ public function testRadioWithValue() ); } + public function testRadioRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, +'/div + [@class="form-group"] + [ + ./small[text() = "[trans]really helpful text[/trans]"] + ] +' + ); + } + public function testButtonAttributeNameRepeatedIfTrue() { $form = $this->factory->createNamed('button', ButtonType::class, null, [ diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php index 8309db19ecd7c..2528cce5b83a7 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\DebugBundle\DependencyInjection; +use Symfony\Bridge\Monolog\Command\ServerLogCommand; use Symfony\Bundle\DebugBundle\Command\ServerDumpPlaceholderCommand; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -90,6 +91,10 @@ public function load(array $configs, ContainerBuilder $container) ]]) ; } + + if (!class_exists(ServerLogCommand::class)) { + $container->removeDefinition('monolog.command.server_log'); + } } /** diff --git a/src/Symfony/Bundle/DebugBundle/LICENSE b/src/Symfony/Bundle/DebugBundle/LICENSE index cf8b3ebe87145..684fbf94df83c 100644 --- a/src/Symfony/Bundle/DebugBundle/LICENSE +++ b/src/Symfony/Bundle/DebugBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2019 Fabien Potencier +Copyright (c) 2014-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml index 49772b46962f7..c7cc5725cf8f8 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -107,5 +107,9 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 8188769bdd29e..46dc89b255395 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -21,6 +21,7 @@ CHANGELOG * Service route loaders must be tagged with `routing.route_loader`. * Added `slugger` service and `SluggerInterface` alias * Removed the `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract` services. + * Removed the `router.cache_class_prefix` parameter. 4.4.0 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php index 5e6277567eff3..290f1da5e3af7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -102,11 +102,12 @@ private function getContainerBuilder(): ContainerBuilder $refl->setAccessible(true); $refl->setValue($parameterBag, true); - $passConfig = $container->getCompilerPassConfig(); - $passConfig->setRemovingPasses([]); - $passConfig->setAfterRemovingPasses([]); - - $skippedIds = $kernelContainer->getRemovedIds(); + $skippedIds = []; + foreach ($container->getServiceIds() as $serviceId) { + if (0 === strpos($serviceId, '.errored.')) { + $skippedIds[$serviceId] = true; + } + } } $container->setParameter('container.build_hash', 'lint_container'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php index cc322847d0262..1210b40ee0b6a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php @@ -101,7 +101,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int (new SymfonyStyle($input, $output)) ->table(['Secret', 'Value'] + (null !== $localSecrets ? [2 => 'Local Value'] : []), $rows); - $io->comment("Local values override secret values.\nUse secrets:set --local to defined them."); + $io->comment("Local values override secret values.\nUse secrets:set --local to define them."); return 0; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index e717b073893b4..c0569281295e2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -23,6 +23,7 @@ use Symfony\Component\Translation\Catalogue\TargetOperation; use Symfony\Component\Translation\Extractor\ExtractorInterface; use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Writer\TranslationWriterInterface; @@ -175,12 +176,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - $errorIo->title('Translation Messages Extractor and Dumper'); - $errorIo->comment(sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); + $io->title('Translation Messages Extractor and Dumper'); + $io->comment(sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); // load any messages from templates $extractedCatalogue = new MessageCatalogue($input->getArgument('locale')); - $errorIo->comment('Parsing templates...'); + $io->comment('Parsing templates...'); $this->extractor->setPrefix($input->getOption('prefix')); foreach ($viewsPaths as $path) { if (is_dir($path) || is_file($path)) { @@ -190,7 +191,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // load any existing messages from the translation files $currentCatalogue = new MessageCatalogue($input->getArgument('locale')); - $errorIo->comment('Loading translation files...'); + $io->comment('Loading translation files...'); foreach ($transPaths as $path) { if (is_dir($path)) { $this->reader->read($path, $currentCatalogue); @@ -216,6 +217,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int $resultMessage = 'Translation files were successfully updated'; + // move new messages to intl domain when possible + if (class_exists(\MessageFormatter::class)) { + foreach ($operation->getDomains() as $domain) { + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + $newMessages = $operation->getNewMessages($domain); + + if ([] === $newMessages || ([] === $currentCatalogue->all($intlDomain) && [] !== $currentCatalogue->all($domain))) { + continue; + } + + $result = $operation->getResult(); + $allIntlMessages = $result->all($intlDomain); + $currentMessages = array_diff_key($newMessages, $result->all($domain)); + $result->replace($currentMessages, $domain); + $result->replace($allIntlMessages + $newMessages, $intlDomain); + } + } + // show compiled list of messages if (true === $input->getOption('dump-messages')) { $extractedMessagesCount = 0; @@ -258,7 +277,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ('xlf' === $input->getOption('output-format')) { - $errorIo->comment(sprintf('Xliff output version is %s', $input->getOption('xliff-version'))); + $io->comment(sprintf('Xliff output version is %s', $input->getOption('xliff-version'))); } $resultMessage = sprintf('%d message%s successfully extracted', $extractedMessagesCount, $extractedMessagesCount > 1 ? 's were' : ' was'); @@ -270,7 +289,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // save the files if (true === $input->getOption('force')) { - $errorIo->comment('Writing files...'); + $io->comment('Writing files...'); $bundleTransPath = false; foreach ($transPaths as $path) { @@ -290,7 +309,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - $errorIo->success($resultMessage.'.'); + $io->success($resultMessage.'.'); return 0; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php index 20f420ce9d45b..d3548430b9f6b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php @@ -38,6 +38,7 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\SerializerInterface; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 8f1b86b3a2229..9d356e5aed31e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -311,6 +311,7 @@ public function load(array $configs, ContainerBuilder $container) $container->removeDefinition('console.command.messenger_failed_messages_retry'); $container->removeDefinition('console.command.messenger_failed_messages_show'); $container->removeDefinition('console.command.messenger_failed_messages_remove'); + $container->removeDefinition('cache.messenger.restart_workers_signal'); } if ($this->httpClientConfigEnabled = $this->isConfigEnabled($container, $config['http_client'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index 037d0f067c36a..a1660aea71f7a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -36,7 +36,14 @@ public function __construct(KernelInterface $kernel, string $cacheDir = null) $this->kernel = $kernel; $this->cacheDir = $cacheDir; - parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge(['debug' => $kernel->isDebug()], $this->getOptions())); + $isDebug = $kernel->isDebug(); + $options = ['debug' => $isDebug]; + + if ($isDebug) { + $options['stale_if_error'] = 0; + } + + parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge($options, $this->getOptions())); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/LICENSE b/src/Symfony/Bundle/FrameworkBundle/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/LICENSE +++ b/src/Symfony/Bundle/FrameworkBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index b8e4488456f2c..95ed47972093c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -412,6 +412,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php index 883a68613c540..047d8d5804009 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php @@ -47,7 +47,9 @@ public function generateKeys(bool $override = false): bool $this->lastMessage = null; if (null === $this->encryptionKey && '' !== $this->decryptionKey = (string) $this->decryptionKey) { - throw new \LogicException('Cannot generate keys when a decryption key has been provided while instantiating the vault.'); + $this->lastMessage = 'Cannot generate keys when a decryption key has been provided while instantiating the vault.'; + + return false; } try { diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php index 478b3983a194d..07cc3831891de 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php @@ -43,6 +43,7 @@ protected function tearDown(): void { static::ensureKernelShutdown(); static::$kernel = null; + static::$booted = false; } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/fragments_and_hinclude.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/fragments_and_hinclude.php new file mode 100644 index 0000000000000..dbcf5b786dba9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/fragments_and_hinclude.php @@ -0,0 +1,8 @@ +loadFromExtension('framework', [ + 'fragments' => [ + 'enabled' => true, + 'hinclude_default_template' => 'global_hinclude_template', + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_disabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_disabled.php new file mode 100644 index 0000000000000..e02542d9778c6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_disabled.php @@ -0,0 +1,5 @@ +loadFromExtension('framework', [ + 'messenger' => false, +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/fragments_and_hinclude.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/fragments_and_hinclude.xml new file mode 100644 index 0000000000000..fb007313b9a71 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/fragments_and_hinclude.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_disabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_disabled.xml new file mode 100644 index 0000000000000..6f57398b30d2b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_disabled.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/fragments_and_hinclude.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/fragments_and_hinclude.yml new file mode 100644 index 0000000000000..b03f37da7946e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/fragments_and_hinclude.yml @@ -0,0 +1,4 @@ +framework: + fragments: + enabled: true + hinclude_default_template: global_hinclude_template diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_disabled.yml new file mode 100644 index 0000000000000..1b2d2d1a4f475 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_disabled.yml @@ -0,0 +1,2 @@ +framework: + messenger: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 0f90116ca10a0..605be2bf63dae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -159,6 +159,13 @@ public function testEsiDisabled() $this->assertFalse($container->hasDefinition('esi')); } + public function testFragmentsAndHinclude() + { + $container = $this->createContainerFromFile('fragments_and_hinclude'); + $this->assertTrue($container->hasParameter('fragment.renderer.hinclude.global_template')); + $this->assertEquals('global_hinclude_template', $container->getParameter('fragment.renderer.hinclude.global_template')); + } + public function testSsi() { $container = $this->createContainerFromFile('full'); @@ -565,9 +572,23 @@ public function testWebLink() $this->assertTrue($container->hasDefinition('web_link.add_link_header_listener')); } + public function testMessengerServicesRemovedWhenDisabled() + { + $container = $this->createContainerFromFile('messenger_disabled'); + $this->assertFalse($container->hasDefinition('console.command.messenger_consume_messages')); + $this->assertFalse($container->hasDefinition('console.command.messenger_debug')); + $this->assertFalse($container->hasDefinition('console.command.messenger_stop_workers')); + $this->assertFalse($container->hasDefinition('console.command.messenger_setup_transports')); + $this->assertFalse($container->hasDefinition('console.command.messenger_failed_messages_retry')); + $this->assertFalse($container->hasDefinition('console.command.messenger_failed_messages_show')); + $this->assertFalse($container->hasDefinition('console.command.messenger_failed_messages_remove')); + $this->assertFalse($container->hasDefinition('cache.messenger.restart_workers_signal')); + } + public function testMessenger() { $container = $this->createContainerFromFile('messenger'); + $this->assertTrue($container->hasDefinition('console.command.messenger_consume_messages')); $this->assertTrue($container->hasAlias('messenger.default_bus')); $this->assertTrue($container->getAlias('messenger.default_bus')->isPublic()); $this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory')); diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 7f0fe56f17665..fc75ace882f87 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -20,6 +20,7 @@ CHANGELOG 4.4.0 ----- + * Added `anonymous: lazy` mode to firewalls to make them (not) start the session as late as possible * Added `migrate_from` option to encoders configuration. * Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.) * Deprecated the usage of "query_string" without a "search_dn" and a "search_password" config key in Ldap factories. @@ -28,7 +29,6 @@ CHANGELOG 4.3.0 ----- - * Added `anonymous: lazy` mode to firewalls to make them (not) start the session as late as possible * Added new encoder types: `auto` (recommended), `native` and `sodium` * The normalization of the cookie names configured in the `logout.delete_cookies` option is deprecated and will be disabled in Symfony 5.0. This affects to cookies diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php index 62be170ddc1d6..e7f9df1221e69 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php @@ -12,7 +12,10 @@ namespace Symfony\Bundle\SecurityBundle\Debug; use Symfony\Bundle\SecurityBundle\EventListener\FirewallListener; +use Symfony\Bundle\SecurityBundle\Security\FirewallContext; +use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Http\Firewall\AbstractListener; /** * Firewall collecting called listeners. @@ -21,7 +24,7 @@ */ final class TraceableFirewallListener extends FirewallListener { - private $wrappedListeners; + private $wrappedListeners = []; public function getWrappedListeners() { @@ -30,14 +33,47 @@ public function getWrappedListeners() protected function callListeners(RequestEvent $event, iterable $listeners) { + $wrappedListeners = []; + $wrappedLazyListeners = []; + foreach ($listeners as $listener) { - $wrappedListener = new WrappedListener($listener); - $wrappedListener($event); - $this->wrappedListeners[] = $wrappedListener->getInfo(); + if ($listener instanceof LazyFirewallContext) { + \Closure::bind(function () use (&$wrappedLazyListeners, &$wrappedListeners) { + $listeners = []; + foreach ($this->listeners as $listener) { + if ($listener instanceof AbstractListener) { + $listener = new WrappedLazyListener($listener); + $listeners[] = $listener; + $wrappedLazyListeners[] = $listener; + } else { + $listeners[] = function (RequestEvent $event) use ($listener, &$wrappedListeners) { + $wrappedListener = new WrappedListener($listener); + $wrappedListener($event); + $wrappedListeners[] = $wrappedListener->getInfo(); + }; + } + } + $this->listeners = $listeners; + }, $listener, FirewallContext::class)(); + + $listener($event); + } else { + $wrappedListener = $listener instanceof AbstractListener ? new WrappedLazyListener($listener) : new WrappedListener($listener); + $wrappedListener($event); + $wrappedListeners[] = $wrappedListener->getInfo(); + } if ($event->hasResponse()) { break; } } + + if ($wrappedLazyListeners) { + foreach ($wrappedLazyListeners as $lazyListener) { + $this->wrappedListeners[] = $lazyListener->getInfo(); + } + } + + $this->wrappedListeners = array_merge($this->wrappedListeners, $wrappedListeners); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php new file mode 100644 index 0000000000000..691c6659d5384 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Debug; + +use Symfony\Component\VarDumper\Caster\ClassStub; + +/** + * @author Robin Chalas + * + * @internal + */ +trait TraceableListenerTrait +{ + private $response; + private $listener; + private $time; + private $stub; + + /** + * Proxies all method calls to the original listener. + */ + public function __call(string $method, array $arguments) + { + return $this->listener->{$method}(...$arguments); + } + + public function getWrappedListener() + { + return $this->listener; + } + + public function getInfo(): array + { + return [ + 'response' => $this->response, + 'time' => $this->time, + 'stub' => $this->stub ?? $this->stub = ClassStub::wrapCallable($this->listener), + ]; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php new file mode 100644 index 0000000000000..67b178f3f6927 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Debug; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Core\Exception\LazyResponseException; +use Symfony\Component\Security\Http\Firewall\AbstractListener; +use Symfony\Component\VarDumper\Caster\ClassStub; + +/** + * Wraps a lazy security listener. + * + * @author Robin Chalas + * + * @internal + */ +final class WrappedLazyListener extends AbstractListener +{ + use TraceableListenerTrait; + + public function __construct(AbstractListener $listener) + { + $this->listener = $listener; + } + + public function supports(Request $request): ?bool + { + return $this->listener->supports($request); + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestEvent $event) + { + $startTime = microtime(true); + + try { + $ret = $this->listener->authenticate($event); + } catch (LazyResponseException $e) { + $this->response = $e->getResponse(); + + throw $e; + } finally { + $this->time = microtime(true) - $startTime; + } + + $this->response = $event->getResponse(); + + return $ret; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php index 6cdb04773c51a..d42a878c9dacd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php @@ -23,11 +23,7 @@ */ final class WrappedListener { - private $response; - private $listener; - private $time; - private $stub; - private static $hasVarDumper; + use TraceableListenerTrait; public function __construct(callable $listener) { @@ -41,47 +37,4 @@ public function __invoke(RequestEvent $event) $this->time = microtime(true) - $startTime; $this->response = $event->getResponse(); } - - /** - * Proxies all method calls to the original listener. - */ - public function __call(string $method, array $arguments) - { - return $this->listener->{$method}(...$arguments); - } - - public function getWrappedListener(): callable - { - return $this->listener; - } - - public function getInfo(): array - { - if (null !== $this->stub) { - // no-op - } elseif (self::$hasVarDumper ?? self::$hasVarDumper = class_exists(ClassStub::class)) { - $this->stub = ClassStub::wrapCallable($this->listener); - } elseif (\is_array($this->listener)) { - $this->stub = (\is_object($this->listener[0]) ? \get_class($this->listener[0]) : $this->listener[0]).'::'.$this->listener[1]; - } elseif ($this->listener instanceof \Closure) { - $r = new \ReflectionFunction($this->listener); - if (false !== strpos($r->name, '{closure}')) { - $this->stub = 'closure'; - } elseif ($class = $r->getClosureScopeClass()) { - $this->stub = $class->name.'::'.$r->name; - } else { - $this->stub = $r->name; - } - } elseif (\is_string($this->listener)) { - $this->stub = $this->listener; - } else { - $this->stub = \get_class($this->listener).'::__invoke'; - } - - return [ - 'response' => $this->response, - 'time' => $this->time, - 'stub' => $this->stub, - ]; - } } diff --git a/src/Symfony/Bundle/SecurityBundle/LICENSE b/src/Symfony/Bundle/SecurityBundle/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Bundle/SecurityBundle/LICENSE +++ b/src/Symfony/Bundle/SecurityBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php index 7bb631b82aae0..5b50def554a5d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php @@ -2,8 +2,10 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User; +use Symfony\Bundle\SecurityBundle\Tests\Functional\UserWithoutEquatable; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -52,6 +54,6 @@ public function refreshUser(UserInterface $user) public function supportsClass($class) { - return 'Symfony\Component\Security\Core\User\User' === $class; + return User::class === $class || UserWithoutEquatable::class === $class; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php index 3a1046b0c4a17..51f56c220d33c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php @@ -33,7 +33,7 @@ public function testUserChangeClearsCookie() $this->assertNotNull($cookieJar->get('REMEMBERME')); $client->request('GET', '/foo'); - $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertRedirect($client->getResponse(), '/login'); $this->assertNull($cookieJar->get('REMEMBERME')); } } diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index a87e4a1f00d25..6465faa6e0bca 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -24,10 +24,10 @@ "symfony/security-core": "^4.4|^5.0", "symfony/security-csrf": "^4.4|^5.0", "symfony/security-guard": "^4.4|^5.0", - "symfony/security-http": "^4.4.1|^5.0.1" + "symfony/security-http": "^4.4.3|^5.0.3" }, "require-dev": { - "doctrine/doctrine-bundle": "^1.5|^2.0", + "doctrine/doctrine-bundle": "^2.0", "symfony/asset": "^4.4|^5.0", "symfony/browser-kit": "^4.4|^5.0", "symfony/console": "^4.4|^5.0", diff --git a/src/Symfony/Bundle/TwigBundle/LICENSE b/src/Symfony/Bundle/TwigBundle/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Bundle/TwigBundle/LICENSE +++ b/src/Symfony/Bundle/TwigBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/WebProfilerBundle/LICENSE b/src/Symfony/Bundle/WebProfilerBundle/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/LICENSE +++ b/src/Symfony/Bundle/WebProfilerBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig index 819711eff774b..983a6f39f062d 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig @@ -94,7 +94,7 @@ {% for transport in events.transports %}
{{ events.messages(transport)|length }} - {{ events.messages(transport)|length == 1 ? 'message' : 'messages' }} + {{ events.messages(transport)|length == 1 ? 'message' : 'messages' }}
{% endfor %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/info.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/info.html.twig index 0227532e1208a..43404393ec360 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/info.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/info.html.twig @@ -4,7 +4,7 @@ 'no_token' : { status: 'error', title: (token|default('') == 'latest') ? 'There are no profiles' : 'Token not found', - message: (token|default('') == 'latest') ? 'No profiles found in the database.' : 'Token "' ~ token|default('') ~ '" was not found in the database.' + message: (token|default('') == 'latest') ? 'No profiles found.' : 'Token "' ~ token|default('') ~ '" not found.' } } %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php index 6572181cadd56..d5479ecefc168 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php @@ -11,9 +11,11 @@ namespace Symfony\Bundle\WebProfilerBundle\Tests\Controller; -use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController; use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; +use Symfony\Bundle\WebProfilerBundle\Tests\Functional\WebProfilerBundleKernel; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; @@ -26,12 +28,100 @@ use Twig\Loader\LoaderInterface; use Twig\Loader\SourceContextLoaderInterface; -class ProfilerControllerTest extends TestCase +class ProfilerControllerTest extends WebTestCase { + public function testHomeActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); + $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->homeAction(); + } + + public function testHomeActionRedirect() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/_profiler/'); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertSame('/_profiler/empty/search/results?limit=10', $client->getResponse()->getTargetUrl()); + } + + public function testPanelActionWithLatestTokenWhenNoTokensExist() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/_profiler/latest'); + + $this->assertStringContainsString('No profiles found.', $client->getResponse()->getContent()); + } + + public function testPanelActionWithLatestToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/'); + $client->request('GET', '/_profiler/latest'); + + $this->assertStringContainsString('kernel:homepageController', $client->getResponse()->getContent()); + } + + public function testPanelActionWithoutValidToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/_profiler/this-token-does-not-exist'); + + $this->assertStringContainsString('Token "this-token-does-not-exist" not found.', $client->getResponse()->getContent()); + } + + public function testPanelActionWithWrongPanel() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/'); + $client->request('GET', '/_profiler/latest?panel=this-panel-does-not-exist'); + + $this->assertSame(404, $client->getResponse()->getStatusCode()); + } + + public function testPanelActionWithValidPanelAndToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/'); + $crawler = $client->request('GET', '/_profiler/latest?panel=router'); + + $this->assertSame('_', $crawler->filter('.metrics .metric .value')->eq(0)->text()); + } + + public function testToolbarActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); + $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->toolbarAction(Request::create('/_wdt/foo-token'), null); + } + /** * @dataProvider getEmptyTokenCases */ - public function testEmptyToken($token) + public function testToolbarActionWithEmptyToken($token) { $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); @@ -119,10 +209,36 @@ public function testReturns404onTokenNotFound($withCsp) $this->assertEquals(404, $response->getStatusCode()); } + public function testSearchBarActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); + $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->searchBarAction(Request::create('/_profiler/search_bar')); + } + + public function testSearchBarActionDefaultPage() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $crawler = $client->request('GET', '/_profiler/search_bar'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + foreach (['ip', 'status_code', 'url', 'token', 'start', 'end'] as $searchCriteria) { + $this->assertSame('', $crawler->filter(sprintf('form input[name="%s"]', $searchCriteria))->text()); + } + } + /** * @dataProvider provideCspVariants */ - public function testSearchResult($withCsp) + public function testSearchResultsAction($withCsp) { $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); $profiler = $this @@ -185,6 +301,67 @@ public function testSearchResult($withCsp) $this->assertEquals(200, $response->getStatusCode()); } + public function testSearchActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); + $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->searchBarAction(Request::create('/_profiler/search')); + } + + public function testSearchActionWithToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/'); + $token = $client->getResponse()->headers->get('x-debug-token'); + $client->request('GET', '/_profiler/search?token='.$token); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertSame('/_profiler/'.$token, $client->getResponse()->getTargetUrl()); + } + + public function testSearchActionWithoutToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + $client->followRedirects(); + + $client->request('GET', '/'); + $token = $client->getResponse()->headers->get('x-debug-token'); + $client->request('GET', '/_profiler/search?ip=&method=GET&status_code=&url=&token=&start=&end=&limit=10'); + + $this->assertStringContainsString('results found', $client->getResponse()->getContent()); + $this->assertStringContainsString(sprintf('%s', $token, $token), $client->getResponse()->getContent()); + } + + public function testPhpinfoActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); + $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->phpinfoAction(Request::create('/_profiler/phpinfo')); + } + + public function testPhpinfoAction() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/_profiler/phpinfo'); + + $this->assertStringContainsString('PHP License', $client->getResponse()->getContent()); + } + public function provideCspVariants() { return [ diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php new file mode 100644 index 0000000000000..0c8778de054e8 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php @@ -0,0 +1,68 @@ +import(__DIR__.'/../../Resources/config/routing/profiler.xml', '/_profiler'); + $routes->import(__DIR__.'/../../Resources/config/routing/wdt.xml', '/_wdt'); + $routes->add('/', 'kernel:homepageController'); + } + + protected function configureContainer(ContainerBuilder $containerBuilder, LoaderInterface $loader) + { + $containerBuilder->loadFromExtension('framework', [ + 'secret' => 'foo-secret', + 'profiler' => ['only_exceptions' => false], + 'session' => ['storage_id' => 'session.storage.mock_file'], + ]); + + $containerBuilder->loadFromExtension('web_profiler', [ + 'toolbar' => true, + 'intercept_redirects' => false, + ]); + } + + public function getCacheDir() + { + return sys_get_temp_dir().'/cache-'.spl_object_hash($this); + } + + public function getLogDir() + { + return sys_get_temp_dir().'/log-'.spl_object_hash($this); + } + + public function homepageController() + { + return new Response('Homepage Controller.'); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index 2be8f7d32e878..a360f97858e69 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -25,7 +25,9 @@ "twig/twig": "^2.10|^3.0" }, "require-dev": { + "symfony/browser-kit": "^4.4|^5.0", "symfony/console": "^4.4|^5.0", + "symfony/css-selector": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", "symfony/stopwatch": "^4.4|^5.0" }, diff --git a/src/Symfony/Component/Asset/LICENSE b/src/Symfony/Component/Asset/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Asset/LICENSE +++ b/src/Symfony/Component/Asset/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/BrowserKit/LICENSE b/src/Symfony/Component/BrowserKit/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/BrowserKit/LICENSE +++ b/src/Symfony/Component/BrowserKit/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php index 363b887b79ac7..a96dc3ab852ff 100644 --- a/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php +++ b/src/Symfony/Component/Cache/DataCollector/CacheDataCollector.php @@ -61,7 +61,7 @@ public function reset() public function lateCollect() { - $this->data = $this->cloneVar($this->data); + $this->data['instances']['calls'] = $this->cloneVar($this->data['instances']['calls']); } /** diff --git a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php index 35bf5e7b456ab..f52d0271e4117 100644 --- a/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php +++ b/src/Symfony/Component/Cache/DependencyInjection/CachePoolPass.php @@ -55,7 +55,7 @@ public function process(ContainerBuilder $container) } $seed .= '.'.$container->getParameter('kernel.container_class'); - $pools = []; + $allPools = []; $clearers = []; $attributes = [ 'provider', @@ -103,7 +103,12 @@ public function process(ContainerBuilder $container) if (ChainAdapter::class === $class) { $adapters = []; foreach ($adapter->getArgument(0) as $provider => $adapter) { - $chainedPool = $adapter = new ChildDefinition($adapter); + if ($adapter instanceof ChildDefinition) { + $chainedPool = $adapter; + } else { + $chainedPool = $adapter = new ChildDefinition($adapter); + } + $chainedTags = [\is_int($provider) ? [] : ['provider' => $provider]]; $chainedClass = ''; @@ -163,7 +168,7 @@ public function process(ContainerBuilder $container) $clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); } - $pools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); + $allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); } $notAliasedCacheClearerId = $this->cacheClearerId; @@ -171,7 +176,7 @@ public function process(ContainerBuilder $container) $this->cacheClearerId = (string) $container->getAlias($this->cacheClearerId); } if ($container->hasDefinition($this->cacheClearerId)) { - $clearers[$notAliasedCacheClearerId] = $pools; + $clearers[$notAliasedCacheClearerId] = $allPools; } foreach ($clearers as $id => $pools) { @@ -189,7 +194,7 @@ public function process(ContainerBuilder $container) } if ($container->hasDefinition('console.command.cache_pool_list')) { - $container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, array_keys($pools)); + $container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, array_keys($allPools)); } } diff --git a/src/Symfony/Component/Cache/LICENSE b/src/Symfony/Component/Cache/LICENSE index 3c464ca94359b..a7ec70801827a 100644 --- a/src/Symfony/Component/Cache/LICENSE +++ b/src/Symfony/Component/Cache/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2016-2019 Fabien Potencier +Copyright (c) 2016-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php index e763dabe48cfa..20701adcb4507 100644 --- a/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php +++ b/src/Symfony/Component/Cache/Tests/DependencyInjection/CachePoolPassTest.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Cache\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; +use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\ChainAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -174,4 +176,42 @@ public function testThrowsExceptionWhenCachePoolTagHasUnknownAttributes() $this->cachePoolPass->process($container); } + + public function testChainAdapterPool() + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.container_class', 'app'); + $container->setParameter('kernel.project_dir', 'foo'); + + $container->register('cache.adapter.array', ArrayAdapter::class) + ->addTag('cache.pool'); + $container->register('cache.adapter.apcu', ApcuAdapter::class) + ->setArguments([null, 0, null]) + ->addTag('cache.pool'); + $container->register('cache.chain', ChainAdapter::class) + ->addArgument(['cache.adapter.array', 'cache.adapter.apcu']) + ->addTag('cache.pool'); + $container->setDefinition('cache.app', new ChildDefinition('cache.chain')) + ->addTag('cache.pool'); + $container->setDefinition('doctrine.result_cache_pool', new ChildDefinition('cache.app')) + ->addTag('cache.pool'); + + $this->cachePoolPass->process($container); + + $appCachePool = $container->getDefinition('cache.app'); + $this->assertInstanceOf(ChildDefinition::class, $appCachePool); + $this->assertSame('cache.chain', $appCachePool->getParent()); + + $chainCachePool = $container->getDefinition('cache.chain'); + $this->assertNotInstanceOf(ChildDefinition::class, $chainCachePool); + $this->assertCount(2, $chainCachePool->getArgument(0)); + $this->assertInstanceOf(ChildDefinition::class, $chainCachePool->getArgument(0)[0]); + $this->assertSame('cache.adapter.array', $chainCachePool->getArgument(0)[0]->getParent()); + $this->assertInstanceOf(ChildDefinition::class, $chainCachePool->getArgument(0)[1]); + $this->assertSame('cache.adapter.apcu', $chainCachePool->getArgument(0)[1]->getParent()); + + $doctrineCachePool = $container->getDefinition('doctrine.result_cache_pool'); + $this->assertInstanceOf(ChildDefinition::class, $doctrineCachePool); + $this->assertSame('cache.app', $doctrineCachePool->getParent()); + } } diff --git a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php index 01400c86ba33f..86ab270cd8e7d 100644 --- a/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php +++ b/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php @@ -35,6 +35,8 @@ private function init(string $namespace, ?string $directory) throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); } $directory .= \DIRECTORY_SEPARATOR.$namespace; + } else { + $directory .= \DIRECTORY_SEPARATOR.'@'; } if (!file_exists($directory)) { @mkdir($directory, 0777, true); diff --git a/src/Symfony/Component/Config/LICENSE b/src/Symfony/Component/Config/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Config/LICENSE +++ b/src/Symfony/Component/Config/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Config/README.md b/src/Symfony/Component/Config/README.md index bf400da196b22..0bbde55230a21 100644 --- a/src/Symfony/Component/Config/README.md +++ b/src/Symfony/Component/Config/README.md @@ -8,7 +8,7 @@ may be (YAML, XML, INI files, or for instance a database). Resources --------- - * [Documentation](https://symfony.com/doc/current/components/config/index.html) + * [Documentation](https://symfony.com/doc/current/components/config.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 38956d1fe06c2..dc7c8af6bf63c 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -648,8 +648,13 @@ public function find(string $name) // filter out aliases for commands which are already on the list if (\count($commands) > 1) { $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands; - $commands = array_unique(array_filter($commands, function ($nameOrAlias) use ($commandList, $commands, &$aliases) { - $commandName = $commandList[$nameOrAlias] instanceof Command ? $commandList[$nameOrAlias]->getName() : $nameOrAlias; + $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) { + if (!$commandList[$nameOrAlias] instanceof Command) { + $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias); + } + + $commandName = $commandList[$nameOrAlias]->getName(); + $aliases[$nameOrAlias] = $commandName; return $commandName === $nameOrAlias || !\in_array($commandName, $commands); @@ -664,10 +669,6 @@ public function find(string $name) $maxLen = max(Helper::strlen($abbrev), $maxLen); } $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) { - if (!$commandList[$cmd] instanceof Command) { - $commandList[$cmd] = $this->commandLoader->get($cmd); - } - if ($commandList[$cmd]->isHidden()) { unset($commands[array_search($cmd, $commands)]); diff --git a/src/Symfony/Component/Console/Helper/FormatterHelper.php b/src/Symfony/Component/Console/Helper/FormatterHelper.php index 75c9b613f14f6..a505415cffdc6 100644 --- a/src/Symfony/Component/Console/Helper/FormatterHelper.php +++ b/src/Symfony/Component/Console/Helper/FormatterHelper.php @@ -48,12 +48,12 @@ public function formatBlock($messages, string $style, bool $large = false) foreach ($messages as $message) { $message = OutputFormatter::escape($message); $lines[] = sprintf($large ? ' %s ' : ' %s ', $message); - $len = max($this->strlen($message) + ($large ? 4 : 2), $len); + $len = max(self::strlen($message) + ($large ? 4 : 2), $len); } $messages = $large ? [str_repeat(' ', $len)] : []; for ($i = 0; isset($lines[$i]); ++$i) { - $messages[] = $lines[$i].str_repeat(' ', $len - $this->strlen($lines[$i])); + $messages[] = $lines[$i].str_repeat(' ', $len - self::strlen($lines[$i])); } if ($large) { $messages[] = str_repeat(' ', $len); @@ -73,17 +73,13 @@ public function formatBlock($messages, string $style, bool $large = false) */ public function truncate(string $message, int $length, string $suffix = '...') { - $computedLength = $length - $this->strlen($suffix); + $computedLength = $length - self::strlen($suffix); - if ($computedLength > $this->strlen($message)) { + if ($computedLength > self::strlen($message)) { return $message; } - if (false === $encoding = mb_detect_encoding($message, null, true)) { - return substr($message, 0, $length).$suffix; - } - - return mb_substr($message, 0, $length, $encoding).$suffix; + return self::substr($message, 0, $length).$suffix; } /** diff --git a/src/Symfony/Component/Console/Input/Input.php b/src/Symfony/Component/Console/Input/Input.php index 8c7905db78916..ff5d3170f4e04 100644 --- a/src/Symfony/Component/Console/Input/Input.php +++ b/src/Symfony/Component/Console/Input/Input.php @@ -104,7 +104,7 @@ public function getArguments() /** * {@inheritdoc} */ - public function getArgument($name) + public function getArgument(string $name) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); @@ -116,7 +116,7 @@ public function getArgument($name) /** * {@inheritdoc} */ - public function setArgument($name, $value) + public function setArgument(string $name, $value) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); diff --git a/src/Symfony/Component/Console/LICENSE b/src/Symfony/Component/Console/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Console/LICENSE +++ b/src/Symfony/Component/Console/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Console/README.md b/src/Symfony/Component/Console/README.md index 664a37c0ee7be..3e2fc605e5bfd 100644 --- a/src/Symfony/Component/Console/README.md +++ b/src/Symfony/Component/Console/README.md @@ -7,7 +7,7 @@ interfaces. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/console/index.html) + * [Documentation](https://symfony.com/doc/current/components/console.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 4bfbc71c5bab4..0081504b77de1 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -626,6 +626,9 @@ public function testFindAlternativeCommandsWithAnAlias() $fooCommand->setAliases(['foo2']); $application = new Application(); + $application->setCommandLoader(new FactoryCommandLoader([ + 'foo3' => static function () use ($fooCommand) { return $fooCommand; }, + ])); $application->add($fooCommand); $result = $application->find('foo'); diff --git a/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php index b7a26f95087f9..467f38b6d45c8 100644 --- a/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/SymfonyQuestionHelperTest.php @@ -145,13 +145,13 @@ public function testChoiceQuestionPadding() ); $this->assertOutputContains(<< EOT - , $output); + , $output, true); } public function testChoiceQuestionCustomPrompt() @@ -168,9 +168,9 @@ public function testChoiceQuestionCustomPrompt() $this->assertOutputContains(<<ccc> + >ccc> EOT - , $output); + , $output, true); } protected function getInputStream($input) @@ -200,10 +200,15 @@ protected function createInputInterfaceMock($interactive = true) return $mock; } - private function assertOutputContains($expected, StreamOutput $output) + private function assertOutputContains($expected, StreamOutput $output, $normalize = false) { rewind($output->getStream()); $stream = stream_get_contents($output->getStream()); + + if ($normalize) { + $stream = str_replace(PHP_EOL, "\n", $stream); + } + $this->assertStringContainsString($expected, $stream); } } diff --git a/src/Symfony/Component/CssSelector/LICENSE b/src/Symfony/Component/CssSelector/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/CssSelector/LICENSE +++ b/src/Symfony/Component/CssSelector/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php index 0743cbbb5ff1a..7c414258b79f2 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -12,6 +12,7 @@ namespace Symfony\Component\DependencyInjection\Compiler; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\EnvNotFoundException; @@ -166,7 +167,7 @@ private function checkType(Definition $checkedDefinition, $value, \ReflectionPar return; } - if ('callable' === $type && method_exists($class, '__invoke')) { + if ('callable' === $type && (\Closure::class === $class || method_exists($class, '__invoke'))) { return; } @@ -219,6 +220,10 @@ private function checkType(Definition $checkedDefinition, $value, \ReflectionPar return; } + if (\in_array($type, ['callable', 'Closure'], true) && $value instanceof ServiceClosureArgument) { + return; + } + if ('iterable' === $type && (\is_array($value) || $value instanceof \Traversable || $value instanceof IteratorArgument)) { return; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php index da909ae489829..7e76064ce6576 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php @@ -78,8 +78,18 @@ public function process(ContainerBuilder $container) if (isset($decoratingDefinitions[$inner])) { $decoratingDefinition = $decoratingDefinitions[$inner]; - $definition->setTags(array_merge($decoratingDefinition->getTags(), $definition->getTags())); - $decoratingDefinition->setTags([]); + + $decoratingTags = $decoratingDefinition->getTags(); + $resetTags = []; + + if (isset($decoratingTags['container.service_locator'])) { + // container.service_locator has special logic and it must not be transferred out to decorators + $resetTags = ['container.service_locator' => $decoratingTags['container.service_locator']]; + unset($decoratingTags['container.service_locator']); + } + + $definition->setTags(array_merge($decoratingTags, $definition->getTags())); + $decoratingDefinition->setTags($resetTags); $decoratingDefinitions[$inner] = $definition; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 8df9f0e25a9fa..dc735f7c1dee5 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -52,7 +52,7 @@ public function __construct() new ValidateEnvPlaceholdersPass(), new ResolveChildDefinitionsPass(), new RegisterServiceSubscribersPass(), - new ResolveParameterPlaceHoldersPass(false), + new ResolveParameterPlaceHoldersPass(false, false), new ResolveFactoryClassPass(), new ResolveNamedArgumentsPass(), new AutowireRequiredMethodsPass(), diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php index 77eff72565b7e..761aa314faa05 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php @@ -114,6 +114,8 @@ protected function processValue($value, bool $isRoot = false) return parent::processValue($value, $isRoot); } + $bindingNames = []; + foreach ($bindings as $key => $binding) { list($bindingValue, $bindingId, $used, $bindingType, $file) = $binding->getValues(); if ($used) { @@ -123,7 +125,11 @@ protected function processValue($value, bool $isRoot = false) $this->unusedBindings[$bindingId] = [$key, $this->currentId, $bindingType, $file]; } - if (preg_match('/^(?:(?:array|bool|float|int|string) )?\$/', $key)) { + if (preg_match('/^(?:(?:array|bool|float|int|string|([^ $]++)) )\$/', $key, $m)) { + $bindingNames[substr($key, \strlen($m[0]))] = $binding; + } + + if (!isset($m[1])) { continue; } @@ -184,11 +190,17 @@ protected function processValue($value, bool $isRoot = false) continue; } - if (!$typeHint || '\\' !== $typeHint[0] || !isset($bindings[$typeHint = substr($typeHint, 1)])) { + if ($typeHint && '\\' === $typeHint[0] && isset($bindings[$typeHint = substr($typeHint, 1)])) { + $arguments[$key] = $this->getBindingValue($bindings[$typeHint]); + continue; } - $arguments[$key] = $this->getBindingValue($bindings[$typeHint]); + if (isset($bindingNames[$parameter->name])) { + $bindingKey = array_search($binding, $bindings, true); + $argumentType = substr($bindingKey, 0, strpos($bindingKey, ' ')); + $this->errorMessages[] = sprintf('Did you forget to add the type "%s" to argument "$%s" of method "%s::%s()"?', $argumentType, $parameter->name, $reflectionMethod->class, $reflectionMethod->name); + } } if ($arguments !== $call[1]) { diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php index 91bba7ddb146a..b1c81ee3a1414 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveParameterPlaceHoldersPass.php @@ -24,10 +24,12 @@ class ResolveParameterPlaceHoldersPass extends AbstractRecursivePass { private $bag; private $resolveArrays; + private $throwOnResolveException; - public function __construct(bool $resolveArrays = true) + public function __construct($resolveArrays = true, $throwOnResolveException = true) { $this->resolveArrays = $resolveArrays; + $this->throwOnResolveException = $throwOnResolveException; } /** @@ -61,7 +63,16 @@ public function process(ContainerBuilder $container) protected function processValue($value, bool $isRoot = false) { if (\is_string($value)) { - $v = $this->bag->resolveValue($value); + try { + $v = $this->bag->resolveValue($value); + } catch (ParameterNotFoundException $e) { + if ($this->throwOnResolveException) { + throw $e; + } + + $v = null; + $this->container->getDefinition($this->currentId)->addError($e->getMessage()); + } return $this->resolveArrays || !$v || !\is_array($v) ? $v : $value; } diff --git a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php index 0e23d93382170..bd25e3f0ec21d 100644 --- a/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php +++ b/src/Symfony/Component/DependencyInjection/EnvVarProcessor.php @@ -30,8 +30,7 @@ class EnvVarProcessor implements EnvVarProcessorInterface public function __construct(ContainerInterface $container, \Traversable $loaders = null) { $this->container = $container; - $this->loaders = new \IteratorIterator($loaders ?? new \ArrayIterator()); - $this->loaders = $this->loaders->getInnerIterator(); + $this->loaders = $loaders ?? new \ArrayIterator(); } /** @@ -141,20 +140,32 @@ public function getEnv(string $prefix, string $name, \Closure $getEnv) } } - $loaders = $this->loaders; - $this->loaders = new \ArrayIterator(); - - try { - while ((false === $env || null === $env) && $loaders->valid()) { - $loader = $loaders->current(); - $loaders->next(); - $this->loadedVars[] = $vars = $loader->loadEnvVars(); - $env = $vars[$name] ?? false; + if (false === $env || null === $env) { + $loaders = $this->loaders; + $this->loaders = new \ArrayIterator(); + + try { + $i = 0; + $ended = true; + $count = $loaders instanceof \Countable ? $loaders->count() : 0; + foreach ($loaders as $loader) { + if (\count($this->loadedVars) > $i++) { + continue; + } + $this->loadedVars[] = $vars = $loader->loadEnvVars(); + if (false !== $env = $vars[$name] ?? false) { + $ended = false; + break; + } + } + if ($ended || $count === $i) { + $loaders = $this->loaders; + } + } catch (ParameterCircularReferenceException $e) { + // skip loaders that need an env var that is not defined + } finally { + $this->loaders = $loaders; } - } catch (ParameterCircularReferenceException $e) { - // skip loaders that need an env var that is not defined - } finally { - $this->loaders = $loaders; } if (false === $env || null === $env) { diff --git a/src/Symfony/Component/DependencyInjection/LICENSE b/src/Symfony/Component/DependencyInjection/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/DependencyInjection/LICENSE +++ b/src/Symfony/Component/DependencyInjection/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/DependencyInjection/README.md b/src/Symfony/Component/DependencyInjection/README.md index 932647f94a903..cb2d4a11c5886 100644 --- a/src/Symfony/Component/DependencyInjection/README.md +++ b/src/Symfony/Component/DependencyInjection/README.md @@ -7,7 +7,7 @@ way objects are constructed in your application. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/dependency_injection/index.html) + * [Documentation](https://symfony.com/doc/current/components/dependency_injection.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php index 13c34e156c67a..350f85296a09c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -681,4 +682,44 @@ public function testProcessSkipSkippedIds() $this->addToAssertionCount(1); } + + public function testProcessHandleClosureForCallable() + { + $closureDefinition = new Definition(\Closure::class); + $closureDefinition->setFactory([\Closure::class, 'fromCallable']); + $closureDefinition->setArguments(['strlen']); + + $container = new ContainerBuilder(); + $container + ->register('foobar', BarMethodCall::class) + ->addMethodCall('setCallable', [$closureDefinition]); + + (new CheckTypeDeclarationsPass(true))->process($container); + + $this->addToAssertionCount(1); + } + + public function testProcessSuccessWhenPassingServiceClosureArgumentToCallable() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setCallable', [new ServiceClosureArgument(new Reference('foo'))]); + + (new CheckTypeDeclarationsPass(true))->process($container); + + $this->addToAssertionCount(1); + } + + public function testProcessSuccessWhenPassingServiceClosureArgumentToClosure() + { + $container = new ContainerBuilder(); + + $container->register('bar', BarMethodCall::class) + ->addMethodCall('setClosure', [new ServiceClosureArgument(new Reference('foo'))]); + + (new CheckTypeDeclarationsPass(true))->process($container); + + $this->addToAssertionCount(1); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php index fbd19ab3f416b..ed111d6d2c0de 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php @@ -223,6 +223,25 @@ public function testProcessMovesTagsFromDecoratedDefinitionToDecoratingDefinitio $this->assertEquals(['bar' => ['attr' => 'baz']], $container->getDefinition('deco2')->getTags()); } + public function testProcessLeavesServiceLocatorTagOnOriginalDefinition() + { + $container = new ContainerBuilder(); + $container + ->register('foo') + ->setTags(['container.service_locator' => [0 => []], 'bar' => ['attr' => 'baz']]) + ; + $container + ->register('baz') + ->setTags(['foobar' => ['attr' => 'bar']]) + ->setDecoratedService('foo') + ; + + $this->process($container); + + $this->assertEquals(['container.service_locator' => [0 => []]], $container->getDefinition('baz.inner')->getTags()); + $this->assertEquals(['bar' => ['attr' => 'baz'], 'foobar' => ['attr' => 'bar']], $container->getDefinition('baz')->getTags()); + } + protected function process(ContainerBuilder $container) { $pass = new DecoratorServicePass(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php index 87a5cf2261b4a..443114b0a23be 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveBindingsPassTest.php @@ -20,6 +20,7 @@ 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; @@ -169,4 +170,19 @@ public function testSyntheticServiceWithBind() $this->assertSame([1 => 'bar'], $container->getDefinition(NamedArgumentsDummy::class)->getArguments()); } + + public function testEmptyBindingTypehint() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Did you forget to add the type "string" to argument "$apiKey" of method "Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy::__construct()"?'); + + $container = new ContainerBuilder(); + $bindings = [ + 'string $apiKey' => new BoundArgument('foo'), + ]; + $definition = $container->register(NamedArgumentsDummy::class, NamedArgumentsDummy::class); + $definition->setBindings($bindings); + $pass = new ResolveBindingsPass(); + $pass->process($container); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php index b5f02de9405e2..2504ff112cf86 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/ResolveParameterPlaceHoldersPassTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; class ResolveParameterPlaceHoldersPassTest extends TestCase { @@ -71,6 +72,31 @@ public function testBindingsShouldBeResolved() $this->assertSame($this->container->getParameterBag()->resolveValue('%env(BAZ)%'), $boundValue); } + public function testParameterNotFoundExceptionsIsThrown() + { + $this->expectException(ParameterNotFoundException::class); + $this->expectExceptionMessage('The service "baz_service_id" has a dependency on a non-existent parameter "non_existent_param".'); + + $containerBuilder = new ContainerBuilder(); + $definition = $containerBuilder->register('baz_service_id'); + $definition->setArgument(0, '%non_existent_param%'); + + $pass = new ResolveParameterPlaceHoldersPass(); + $pass->process($containerBuilder); + } + + public function testParameterNotFoundExceptionsIsNotThrown() + { + $containerBuilder = new ContainerBuilder(); + $definition = $containerBuilder->register('baz_service_id'); + $definition->setArgument(0, '%non_existent_param%'); + + $pass = new ResolveParameterPlaceHoldersPass(true, false); + $pass->process($containerBuilder); + + $this->assertCount(1, $definition->getErrors()); + } + private function createContainerBuilder(): ContainerBuilder { $containerBuilder = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php index 9970eb474f6be..df329b4f7e316 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/EnvVarProcessorTest.php @@ -3,10 +3,12 @@ namespace Symfony\Component\DependencyInjection\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; use Symfony\Component\DependencyInjection\EnvVarProcessor; +use Symfony\Component\DependencyInjection\Exception\ParameterCircularReferenceException; class EnvVarProcessorTest extends TestCase { @@ -553,4 +555,44 @@ public function loadEnvVars(): array $result = $processor->getEnv('string', 'FOO_ENV_LOADER', function () {}); $this->assertSame('123', $result); // check twice } + + public function testCircularEnvLoader() + { + $container = new ContainerBuilder(); + $container->setParameter('env(FOO_CONTAINER)', 'foo'); + $container->compile(); + + $index = 0; + $loaders = function () use (&$index) { + if (0 === $index++) { + throw new ParameterCircularReferenceException(['FOO_CONTAINER']); + } + + yield new class() implements EnvVarLoaderInterface { + public function loadEnvVars(): array + { + return [ + 'FOO_ENV_LOADER' => '123', + ]; + } + }; + }; + + $processor = new EnvVarProcessor($container, new RewindableGenerator($loaders, 1)); + + $result = $processor->getEnv('string', 'FOO_CONTAINER', function () {}); + $this->assertSame('foo', $result); + + $result = $processor->getEnv('string', 'FOO_ENV_LOADER', function () {}); + $this->assertSame('123', $result); + + $result = $processor->getEnv('default', ':BAR_CONTAINER', function ($name) use ($processor) { + $this->assertSame('BAR_CONTAINER', $name); + + return $processor->getEnv('string', $name, function () {}); + }); + $this->assertNull($result); + + $this->assertSame(2, $index); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php index c308ef9545710..69f1a693a4c5b 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarMethodCall.php @@ -36,4 +36,12 @@ public function setArray(array $array) public function setIterable(iterable $iterable) { } + + public function setCallable(callable $callable): void + { + } + + public function setClosure(\Closure $closure): void + { + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php index e184fad7bdf0c..4f9074e9aaf7f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/services_subscriber.php @@ -45,8 +45,8 @@ public function isCompiled(): bool public function getRemovedIds(): array { return [ - '.service_locator.bPEFRiK' => true, - '.service_locator.bPEFRiK.foo_service' => true, + '.service_locator.2Wk0Efb' => true, + '.service_locator.2Wk0Efb.foo_service' => true, 'Psr\\Container\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\ContainerInterface' => true, 'Symfony\\Component\\DependencyInjection\\Tests\\Fixtures\\CustomDefinition' => true, diff --git a/src/Symfony/Component/DependencyInjection/TypedReference.php b/src/Symfony/Component/DependencyInjection/TypedReference.php index 3765bc73f83a0..78d8edf0b03f6 100644 --- a/src/Symfony/Component/DependencyInjection/TypedReference.php +++ b/src/Symfony/Component/DependencyInjection/TypedReference.php @@ -20,7 +20,6 @@ class TypedReference extends Reference { private $type; private $name; - private $requiringClass; /** * @param string $id The service identifier diff --git a/src/Symfony/Component/DomCrawler/LICENSE b/src/Symfony/Component/DomCrawler/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/DomCrawler/LICENSE +++ b/src/Symfony/Component/DomCrawler/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Dotenv/Dotenv.php b/src/Symfony/Component/Dotenv/Dotenv.php index eb17855427cc4..88515144518ac 100644 --- a/src/Symfony/Component/Dotenv/Dotenv.php +++ b/src/Symfony/Component/Dotenv/Dotenv.php @@ -272,7 +272,10 @@ private function lexValue(): string $this->cursor += 1 + $len; } elseif ('"' === $this->data[$this->cursor]) { $value = ''; - ++$this->cursor; + + if (++$this->cursor === $this->end) { + throw $this->createFormatException('Missing quote to end the value'); + } while ('"' !== $this->data[$this->cursor] || ('\\' === $this->data[$this->cursor - 1] && '\\' !== $this->data[$this->cursor - 2])) { $value .= $this->data[$this->cursor]; diff --git a/src/Symfony/Component/Dotenv/LICENSE b/src/Symfony/Component/Dotenv/LICENSE index 3c464ca94359b..a7ec70801827a 100644 --- a/src/Symfony/Component/Dotenv/LICENSE +++ b/src/Symfony/Component/Dotenv/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2016-2019 Fabien Potencier +Copyright (c) 2016-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php index b1d7e7b37aa43..1ae3eefa94592 100644 --- a/src/Symfony/Component/Dotenv/Tests/DotenvTest.php +++ b/src/Symfony/Component/Dotenv/Tests/DotenvTest.php @@ -40,6 +40,7 @@ public function getEnvDataWithFormatErrors() ['FOO', "Missing = in the environment variable declaration in \".env\" at line 1.\n...FOO...\n ^ line 1 offset 3"], ['FOO="foo', "Missing quote to end the value in \".env\" at line 1.\n...FOO=\"foo...\n ^ line 1 offset 8"], ['FOO=\'foo', "Missing quote to end the value in \".env\" at line 1.\n...FOO='foo...\n ^ line 1 offset 8"], + ["FOO=\"foo\nBAR=\"bar\"", "Missing quote to end the value in \".env\" at line 1.\n...FOO=\"foo\\nBAR=\"bar\"...\n ^ line 1 offset 18"], ['FOO=\'foo'."\n", "Missing quote to end the value in \".env\" at line 1.\n...FOO='foo\\n...\n ^ line 1 offset 9"], ['export FOO', "Unable to unset an environment variable in \".env\" at line 1.\n...export FOO...\n ^ line 1 offset 10"], ['FOO=${FOO', "Unclosed braces on variable expansion in \".env\" at line 1.\n...FOO=\${FOO...\n ^ line 1 offset 9"], @@ -448,10 +449,13 @@ public function testGetVariablesValueFromGetenv() putenv('Foo=Bar'); $dotenv = new Dotenv(true); - $values = $dotenv->parse('Foo=${Foo}'); - $this->assertSame('Bar', $values['Foo']); - putenv('Foo'); + try { + $values = $dotenv->parse('Foo=${Foo}'); + $this->assertSame('Bar', $values['Foo']); + } finally { + putenv('Foo'); + } } public function testNoDeprecationWarning() diff --git a/src/Symfony/Component/ErrorHandler/LICENSE b/src/Symfony/Component/ErrorHandler/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/ErrorHandler/LICENSE +++ b/src/Symfony/Component/ErrorHandler/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php index 3098200b01604..1e09afbb9f320 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorEnhancer/ClassNotFoundErrorEnhancerTest.php @@ -30,6 +30,10 @@ public static function setUpBeforeClass(): void // get class loaders wrapped by DebugClassLoader if ($function[0] instanceof DebugClassLoader) { $function = $function[0]->getClassLoader(); + + if (!\is_array($function)) { + continue; + } } if ($function[0] instanceof ComposerClassLoader) { diff --git a/src/Symfony/Component/EventDispatcher/EventDispatcher.php b/src/Symfony/Component/EventDispatcher/EventDispatcher.php index c0f839b9db905..c639e0b585082 100644 --- a/src/Symfony/Component/EventDispatcher/EventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/EventDispatcher.php @@ -240,7 +240,7 @@ private function sortListeners(string $eventName) $this->sorted[$eventName] = []; foreach ($this->listeners[$eventName] as &$listeners) { - foreach ($listeners as $k => $listener) { + foreach ($listeners as $k => &$listener) { if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { $listener[0] = $listener[0](); $listener[1] = $listener[1] ?? '__invoke'; diff --git a/src/Symfony/Component/EventDispatcher/LICENSE b/src/Symfony/Component/EventDispatcher/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/EventDispatcher/LICENSE +++ b/src/Symfony/Component/EventDispatcher/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/EventDispatcher/README.md b/src/Symfony/Component/EventDispatcher/README.md index 185c3fecf8fee..e0d38eed017f8 100644 --- a/src/Symfony/Component/EventDispatcher/README.md +++ b/src/Symfony/Component/EventDispatcher/README.md @@ -8,7 +8,7 @@ them. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/event_dispatcher/index.html) + * [Documentation](https://symfony.com/doc/current/components/event_dispatcher.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/ExpressionLanguage/LICENSE b/src/Symfony/Component/ExpressionLanguage/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/ExpressionLanguage/LICENSE +++ b/src/Symfony/Component/ExpressionLanguage/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 76c10b015bccd..3fa4d6bb51288 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -206,11 +206,13 @@ public function chmod($files, int $mode, int $umask = 0000, bool $recursive = fa /** * Change the owner of an array of files or directories. * - * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change owner + * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change owner + * @param string|int $user A user name or number + * @param bool $recursive Whether change the owner recursively or not * * @throws IOException When the change fails */ - public function chown($files, string $user, bool $recursive = false) + public function chown($files, $user, bool $recursive = false) { foreach ($this->toIterable($files) as $file) { if ($recursive && is_dir($file) && !is_link($file)) { @@ -231,11 +233,13 @@ public function chown($files, string $user, bool $recursive = false) /** * Change the group of an array of files or directories. * - * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change group + * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change group + * @param string|int $group A group name or number + * @param bool $recursive Whether change the group recursively or not * * @throws IOException When the change fails */ - public function chgrp($files, string $group, bool $recursive = false) + public function chgrp($files, $group, bool $recursive = false) { foreach ($this->toIterable($files) as $file) { if ($recursive && is_dir($file) && !is_link($file)) { diff --git a/src/Symfony/Component/Filesystem/LICENSE b/src/Symfony/Component/Filesystem/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Filesystem/LICENSE +++ b/src/Symfony/Component/Filesystem/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Filesystem/README.md b/src/Symfony/Component/Filesystem/README.md index 877ab3543f32d..cb03d43c15dd2 100644 --- a/src/Symfony/Component/Filesystem/README.md +++ b/src/Symfony/Component/Filesystem/README.md @@ -6,7 +6,7 @@ The Filesystem component provides basic utilities for the filesystem. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/filesystem/index.html) + * [Documentation](https://symfony.com/doc/current/components/filesystem.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index d4299da4d93f3..dcd3c6140693e 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -521,7 +521,7 @@ public function testChmodChangesZeroModeOnSubdirectoriesOnRecursive() $this->assertFilePermissions(753, $subdirectory); } - public function testChown() + public function testChownByName() { $this->markAsSkippedIfPosixIsMissing(); @@ -534,7 +534,20 @@ public function testChown() $this->assertSame($owner, $this->getFileOwner($dir)); } - public function testChownRecursive() + public function testChownById() + { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace.\DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + + $ownerId = $this->getFileOwnerId($dir); + $this->filesystem->chown($dir, $ownerId); + + $this->assertSame($ownerId, $this->getFileOwnerId($dir)); + } + + public function testChownRecursiveByName() { $this->markAsSkippedIfPosixIsMissing(); @@ -549,6 +562,21 @@ public function testChownRecursive() $this->assertSame($owner, $this->getFileOwner($file)); } + public function testChownRecursiveById() + { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace.\DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + $file = $dir.\DIRECTORY_SEPARATOR.'file'; + touch($file); + + $ownerId = $this->getFileOwnerId($dir); + $this->filesystem->chown($dir, $ownerId, true); + + $this->assertSame($ownerId, $this->getFileOwnerId($file)); + } + public function testChownSymlink() { $this->markAsSkippedIfSymlinkIsMissing(); @@ -624,7 +652,7 @@ public function testChownFail() $this->filesystem->chown($dir, 'user'.time().mt_rand(1000, 9999)); } - public function testChgrp() + public function testChgrpByName() { $this->markAsSkippedIfPosixIsMissing(); @@ -637,6 +665,19 @@ public function testChgrp() $this->assertSame($group, $this->getFileGroup($dir)); } + public function testChgrpById() + { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace.\DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + + $groupId = $this->getFileGroupId($dir); + $this->filesystem->chgrp($dir, $groupId); + + $this->assertSame($groupId, $this->getFileGroupId($dir)); + } + public function testChgrpRecursive() { $this->markAsSkippedIfPosixIsMissing(); @@ -652,7 +693,7 @@ public function testChgrpRecursive() $this->assertSame($group, $this->getFileGroup($file)); } - public function testChgrpSymlink() + public function testChgrpSymlinkByName() { $this->markAsSkippedIfSymlinkIsMissing(); @@ -669,6 +710,23 @@ public function testChgrpSymlink() $this->assertSame($group, $this->getFileGroup($link)); } + public function testChgrpSymlinkById() + { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace.\DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.\DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->symlink($file, $link); + + $groupId = $this->getFileGroupId($link); + $this->filesystem->chgrp($link, $groupId); + + $this->assertSame($groupId, $this->getFileGroupId($link)); + } + public function testChgrpLink() { $this->markAsSkippedIfLinkIsMissing(); diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php index 08afefbb24eff..396a3ab2e8634 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php @@ -105,21 +105,36 @@ protected function assertFilePermissions($expectedFilePerms, $filePath) ); } + protected function getFileOwnerId($filepath) + { + $this->markAsSkippedIfPosixIsMissing(); + + $infos = stat($filepath); + + return $infos['uid']; + } + protected function getFileOwner($filepath) { $this->markAsSkippedIfPosixIsMissing(); + return ($datas = posix_getpwuid($this->getFileOwnerId($filepath))) ? $datas['name'] : null; + } + + protected function getFileGroupId($filepath) + { + $this->markAsSkippedIfPosixIsMissing(); + $infos = stat($filepath); - return ($datas = posix_getpwuid($infos['uid'])) ? $datas['name'] : null; + return $infos['gid']; } protected function getFileGroup($filepath) { $this->markAsSkippedIfPosixIsMissing(); - $infos = stat($filepath); - if ($datas = posix_getgrgid($infos['gid'])) { + if ($datas = posix_getgrgid($this->getFileGroupId($filepath))) { return $datas['name']; } diff --git a/src/Symfony/Component/Finder/LICENSE b/src/Symfony/Component/Finder/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Finder/LICENSE +++ b/src/Symfony/Component/Finder/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Form/LICENSE b/src/Symfony/Component/Form/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Form/LICENSE +++ b/src/Symfony/Component/Form/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Form/README.md b/src/Symfony/Component/Form/README.md index 3fe6f8bbf3206..5519cd00058dd 100644 --- a/src/Symfony/Component/Form/README.md +++ b/src/Symfony/Component/Form/README.md @@ -6,7 +6,7 @@ The Form component allows you to easily create, process and reuse HTML forms. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/form/index.html) + * [Documentation](https://symfony.com/doc/current/components/form.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php index 46733786a86c1..3f6294c53b3d6 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Constraints/FormValidatorTest.php @@ -135,7 +135,10 @@ public function testDontValidateIfParentWithoutValidConstraint() $parent->add($form); $form->setData($object); + $parent->submit([]); + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); @@ -190,10 +193,15 @@ public function testDontValidateIfNoValidationGroups() 'validation_groups' => [], ]) ->setData($object) + ->setCompound(true) + ->setDataMapper(new PropertyPathMapper()) ->getForm(); $form->setData($object); + $form->submit([]); + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); @@ -216,6 +224,8 @@ public function testDontValidateConstraintsIfNoValidationGroups() // Launch transformer $form->submit('foo'); + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); @@ -238,6 +248,8 @@ public function testDontValidateChildConstraintsIfCallableNoValidationGroups() $form->add($child); $form->submit([]); + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); @@ -266,6 +278,8 @@ function () { throw new TransformationFailedException(); } // Launch transformer $form->submit('foo'); + $this->assertTrue($form->isSubmitted()); + $this->assertFalse($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); @@ -301,6 +315,8 @@ function () { throw new TransformationFailedException(); } // Launch transformer $form->submit('foo'); + $this->assertTrue($form->isSubmitted()); + $this->assertFalse($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); @@ -412,6 +428,8 @@ function () { throw new TransformationFailedException(); } // Launch transformer $form->submit(['child' => 'foo']); + $this->assertTrue($form->isSubmitted()); + $this->assertFalse($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); @@ -617,7 +635,10 @@ public function testDontWalkScalars() $form = $this->getBuilder() ->setData('scalar') ->getForm(); + $form->submit('foo'); + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); @@ -635,6 +656,8 @@ public function testViolationIfExtraData() $form->submit(['foo' => 'bar']); + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); @@ -656,6 +679,8 @@ public function testViolationFormatIfMultipleExtraFields() $form->submit(['foo' => 'bar', 'baz' => 'qux', 'quux' => 'quuz']); + $this->assertTrue($form->isSubmitted()); + $this->assertTrue($form->isSynchronized()); $this->expectNoValidate(); $this->validator->validate($form, new Form()); diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index d26b48c19a803..b076e9a7b5209 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -116,7 +116,7 @@ public function request(string $method, string $url, array $options = []): Respo $url = implode('', $url); if (!isset($options['normalized_headers']['user-agent'])) { - $options['normalized_headers']['user-agent'][] = $options['headers'][] = 'User-Agent: Symfony HttpClient/Curl'; + $options['headers'][] = 'User-Agent: Symfony HttpClient/Curl'; } $curlopts = [ @@ -217,8 +217,8 @@ public function request(string $method, string $url, array $options = []): Respo $curlopts[CURLOPT_NOSIGNAL] = true; } - if (!isset($options['normalized_headers']['accept-encoding']) && CURL_VERSION_LIBZ & self::$curlVersion['features']) { - $curlopts[CURLOPT_ENCODING] = 'gzip'; // Expose only one encoding, some servers mess up when more are provided + if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) { + $options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided } foreach ($options['headers'] as $header) { @@ -288,7 +288,7 @@ public function request(string $method, string $url, array $options = []): Respo $pushedResponse = $pushedResponse->response; $pushedResponse->__construct($this->multi, $url, $options, $this->logger); } else { - $this->logger && $this->logger->debug(sprintf('Rejecting pushed response: "%s".', $url)); + $this->logger && $this->logger->debug(sprintf('Rejecting pushed response: "%s"', $url)); $pushedResponse = null; } } @@ -412,7 +412,7 @@ private static function acceptPushForRequest(string $method, array $options, Pus return false; } - foreach (['proxy', 'no_proxy', 'bindto'] as $k) { + foreach (['proxy', 'no_proxy', 'bindto', 'local_cert', 'local_pk'] as $k) { if ($options[$k] !== $pushedResponse->parentOptions[$k]) { return false; } diff --git a/src/Symfony/Component/HttpClient/HttplugClient.php b/src/Symfony/Component/HttpClient/HttplugClient.php index e756ea5d3f434..08023b9c42718 100644 --- a/src/Symfony/Component/HttpClient/HttplugClient.php +++ b/src/Symfony/Component/HttpClient/HttplugClient.php @@ -16,6 +16,7 @@ use Http\Client\Exception\RequestException; use Http\Client\HttpAsyncClient; use Http\Client\HttpClient as HttplugInterface; +use Http\Discovery\Exception\NotFoundException; use Http\Discovery\Psr17FactoryDiscovery; use Http\Message\RequestFactory; use Http\Message\StreamFactory; @@ -75,9 +76,13 @@ public function __construct(HttpClientInterface $client = null, ResponseFactoryI throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".'); } - $psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null; - $this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory(); - $this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory(); + try { + $psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null; + $this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory(); + $this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory(); + } catch (NotFoundException $e) { + throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e); + } } $this->waitLoop = new HttplugWaitLoop($this->client, $this->promisePool, $this->responseFactory, $this->streamFactory); diff --git a/src/Symfony/Component/HttpClient/Internal/NativeClientState.php b/src/Symfony/Component/HttpClient/Internal/NativeClientState.php index e82ce4c853d21..6578929dc5466 100644 --- a/src/Symfony/Component/HttpClient/Internal/NativeClientState.php +++ b/src/Symfony/Component/HttpClient/Internal/NativeClientState.php @@ -11,8 +11,6 @@ namespace Symfony\Component\HttpClient\Internal; -use Symfony\Component\HttpClient\Response\NativeResponse; - /** * Internal representation of the native client's state. * @@ -24,8 +22,6 @@ final class NativeClientState extends ClientState { /** @var int */ public $id; - /** @var NativeResponse[] */ - public $pendingResponses = []; /** @var int */ public $maxHostConnections = PHP_INT_MAX; /** @var int */ diff --git a/src/Symfony/Component/HttpClient/LICENSE b/src/Symfony/Component/HttpClient/LICENSE index 3f853aaf35fe1..69d925ba7511e 100644 --- a/src/Symfony/Component/HttpClient/LICENSE +++ b/src/Symfony/Component/HttpClient/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 Fabien Potencier +Copyright (c) 2018-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index 745f81fb2a55a..d60f5415271ce 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -77,7 +77,7 @@ public function request(string $method, string $url, array $options = []): Respo $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded'; } - if ($gzipEnabled = \extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) { + if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) { // gzip is the most widely available algo, no need to deal with deflate $options['headers'][] = 'Accept-Encoding: gzip'; } @@ -169,7 +169,7 @@ public function request(string $method, string $url, array $options = []): Respo $this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache; } - $this->logger && $this->logger->info(sprintf('Request: %s %s', $method, implode('', $url))); + $this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, implode('', $url))); [$host, $port, $url['authority']] = self::dnsResolve($url, $this->multi, $info, $onProgress); @@ -187,7 +187,7 @@ public function request(string $method, string $url, array $options = []): Respo $context = [ 'http' => [ - 'protocol_version' => $options['http_version'] ?: '1.1', + 'protocol_version' => min($options['http_version'] ?: '1.1', '1.1'), 'method' => $method, 'content' => $options['body'], 'ignore_errors' => true, @@ -227,7 +227,7 @@ public function request(string $method, string $url, array $options = []): Respo $context = stream_context_create($context, ['notification' => $notification]); self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, $noProxy); - return new NativeResponse($this->multi, $context, implode('', $url), $options, $gzipEnabled, $info, $resolveRedirect, $onProgress, $this->logger); + return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolveRedirect, $onProgress, $this->logger); } /** @@ -357,7 +357,7 @@ private static function createRedirectResolver(array $options, string $host, ?ar }); if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) { - $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) { + $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) { return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:'); }); } diff --git a/src/Symfony/Component/HttpClient/Psr18Client.php b/src/Symfony/Component/HttpClient/Psr18Client.php index ba59dc11f7ecc..67c2fdb8f07bc 100644 --- a/src/Symfony/Component/HttpClient/Psr18Client.php +++ b/src/Symfony/Component/HttpClient/Psr18Client.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpClient; +use Http\Discovery\Exception\NotFoundException; use Http\Discovery\Psr17FactoryDiscovery; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\Request; @@ -68,9 +69,13 @@ public function __construct(HttpClientInterface $client = null, ResponseFactoryI throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".'); } - $psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null; - $this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory(); - $this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory(); + try { + $psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null; + $this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory(); + $this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory(); + } catch (NotFoundException $e) { + throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e); + } } /** diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index 618858728839f..e1699b9b2b7ba 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -52,6 +52,7 @@ public function __construct(CurlClientState $multi, $ch, array $options = null, $this->id = $id = (int) $ch; $this->logger = $logger; + $this->shouldBuffer = $options['buffer'] ?? true; $this->timeout = $options['timeout'] ?? null; $this->info['http_method'] = $method; $this->info['user_data'] = $options['user_data'] ?? null; @@ -65,50 +66,25 @@ public function __construct(CurlClientState $multi, $ch, array $options = null, curl_setopt($ch, CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter } - if (null === $content = &$this->content) { - $content = null === $options || true === $options['buffer'] ? fopen('php://temp', 'w+') : (\is_resource($options['buffer']) ? $options['buffer'] : null); - } else { - // Move the pushed response to the activity list - $buffer = $options['buffer']; - - if ('H' !== curl_getinfo($ch, CURLINFO_PRIVATE)[0]) { - if ($options['buffer'] instanceof \Closure) { - try { - [$content, $buffer] = [null, $content]; - [$content, $buffer] = [$buffer, $options['buffer']($headers)]; - } catch (\Throwable $e) { - $multi->handlesActivity[$id][] = null; - $multi->handlesActivity[$id][] = $e; - [$content, $buffer] = [$buffer, false]; - } - } - - if (ftell($content)) { - rewind($content); - $multi->handlesActivity[$id][] = stream_get_contents($content); - } - } - - if (\is_resource($buffer)) { - $content = $buffer; - } elseif (true !== $buffer) { - $content = null; - } - } - - curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger, &$content): int { - return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger, $content); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int { + return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); }); if (null === $options) { // Pushed response: buffer until requested - curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use (&$content): int { - return fwrite($content, $data); + curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int { + $multi->handlesActivity[$id][] = $data; + curl_pause($ch, CURLPAUSE_RECV); + + return \strlen($data); }); return; } + $this->inflate = !isset($options['normalized_headers']['accept-encoding']); + curl_pause($ch, CURLPAUSE_CONT); + if ($onProgress = $options['on_progress']) { $url = isset($info['url']) ? ['url' => $info['url']] : []; curl_setopt($ch, CURLOPT_NOPROGRESS, false); @@ -128,33 +104,16 @@ public function __construct(CurlClientState $multi, $ch, array $options = null, }); } - curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use (&$content, $multi, $id): int { + curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int { $multi->handlesActivity[$id][] = $data; - return null !== $content ? fwrite($content, $data) : \strlen($data); + return \strlen($data); }); $this->initializer = static function (self $response) { - if (null !== $response->info['error']) { - throw new TransportException($response->info['error']); - } - $waitFor = curl_getinfo($ch = $response->handle, CURLINFO_PRIVATE); - if ('H' === $waitFor[0] || 'D' === $waitFor[0]) { - try { - foreach (self::stream([$response]) as $chunk) { - if ($chunk->isFirst()) { - break; - } - } - } catch (\Throwable $e) { - // Persist timeouts thrown during initialization - $response->info['error'] = $e->getMessage(); - $response->close(); - throw $e; - } - } + return 'H' === $waitFor[0] || 'D' === $waitFor[0]; }; // Schedule the request in a non-blocking way @@ -241,6 +200,7 @@ public function __destruct() */ private function close(): void { + $this->inflate = null; unset($this->multi->openHandles[$this->id], $this->multi->handlesActivity[$this->id]); curl_setopt($this->handle, CURLOPT_PRIVATE, '_0'); @@ -419,22 +379,6 @@ private static function parseHeaderLine($ch, string $data, array &$info, array & } curl_setopt($ch, CURLOPT_PRIVATE, $waitFor); - - try { - if (!$content && $options['buffer'] instanceof \Closure && $content = $options['buffer']($headers) ?: null) { - $content = \is_resource($content) ? $content : fopen('php://temp', 'w+'); - } - - if (null !== $info['error']) { - throw new TransportException($info['error']); - } - } catch (\Throwable $e) { - $multi->handlesActivity[$id] = $multi->handlesActivity[$id] ?? [new FirstChunk()]; - $multi->handlesActivity[$id][] = null; - $multi->handlesActivity[$id][] = $e; - - return 0; - } } elseif (null !== $info['redirect_url'] && $logger) { $logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url'])); } diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index a4209149589fe..b458665d44edd 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -94,6 +94,7 @@ public function cancel(): void */ protected function close(): void { + $this->inflate = null; $this->body = []; } @@ -105,22 +106,9 @@ public static function fromRequest(string $method, string $url, array $options, $response = new self([]); $response->requestOptions = $options; $response->id = ++self::$idSequence; - - if (!($options['buffer'] ?? null) instanceof \Closure) { - $response->content = true === ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : (\is_resource($options['buffer']) ? $options['buffer'] : null); - } + $response->shouldBuffer = $options['buffer'] ?? true; $response->initializer = static function (self $response) { - if (null !== $response->info['error']) { - throw new TransportException($response->info['error']); - } - - if (\is_array($response->body[0] ?? null)) { - foreach (self::stream([$response]) as $chunk) { - if ($chunk->isFirst()) { - break; - } - } - } + return \is_array($response->body[0] ?? null); }; $response->info['redirect_count'] = 0; @@ -198,11 +186,6 @@ protected static function perform(ClientState $multi, array &$responses): void } else { // Data or timeout chunk $multi->handlesActivity[$id][] = $chunk; - - if (\is_string($chunk) && null !== $response->content) { - // Buffer response body - fwrite($response->content, $chunk); - } } } } diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index 2e8bce58af198..7abe18abab2e4 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -32,7 +32,6 @@ final class NativeResponse implements ResponseInterface private $onProgress; private $remaining; private $buffer; - private $inflate; private $multi; private $debugBuffer; private $shouldBuffer; @@ -40,7 +39,7 @@ final class NativeResponse implements ResponseInterface /** * @internal */ - public function __construct(NativeClientState $multi, $context, string $url, $options, bool $gzipEnabled, array &$info, callable $resolveRedirect, ?callable $onProgress, ?LoggerInterface $logger) + public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolveRedirect, ?callable $onProgress, ?LoggerInterface $logger) { $this->multi = $multi; $this->id = (int) $context; @@ -51,28 +50,17 @@ public function __construct(NativeClientState $multi, $context, string $url, $op $this->info = &$info; $this->resolveRedirect = $resolveRedirect; $this->onProgress = $onProgress; - $this->content = true === $options['buffer'] ? fopen('php://temp', 'w+') : (\is_resource($options['buffer']) ? $options['buffer'] : null); - $this->shouldBuffer = $options['buffer'] instanceof \Closure ? $options['buffer'] : null; + $this->inflate = !isset($options['normalized_headers']['accept-encoding']); + $this->shouldBuffer = $options['buffer'] ?? true; - // Temporary resources to dechunk/inflate the response stream + // Temporary resource to dechunk the response stream $this->buffer = fopen('php://temp', 'w+'); - $this->inflate = $gzipEnabled ? inflate_init(ZLIB_ENCODING_GZIP) : null; $info['user_data'] = $options['user_data']; ++$multi->responseCount; $this->initializer = static function (self $response) { - if (null !== $response->info['error']) { - throw new TransportException($response->info['error']); - } - - if (null === $response->remaining) { - foreach (self::stream([$response]) as $chunk) { - if ($chunk->isFirst()) { - break; - } - } - } + return null === $response->remaining; }; } @@ -162,14 +150,14 @@ private function open(): void restore_error_handler(); } - stream_set_blocking($h, false); - $this->context = $this->resolveRedirect = null; - - if (isset($context['ssl']['peer_certificate_chain'])) { + if (isset($context['ssl']['capture_peer_cert_chain']) && isset(($context = stream_context_get_options($this->context))['ssl']['peer_certificate_chain'])) { $this->info['peer_certificate_chain'] = $context['ssl']['peer_certificate_chain']; } - // Create dechunk and inflate buffers + stream_set_blocking($h, false); + $this->context = $this->resolveRedirect = null; + + // Create dechunk buffers if (isset($this->headers['content-length'])) { $this->remaining = (int) $this->headers['content-length'][0]; } elseif ('chunked' === ($this->headers['transfer-encoding'][0] ?? null)) { @@ -179,27 +167,6 @@ private function open(): void $this->remaining = -2; } - if ($this->inflate && 'gzip' !== ($this->headers['content-encoding'][0] ?? null)) { - $this->inflate = null; - } - - try { - if (null !== $this->shouldBuffer && null === $this->content && $this->content = ($this->shouldBuffer)($this->headers) ?: null) { - $this->content = \is_resource($this->content) ? $this->content : fopen('php://temp', 'w+'); - } - - if (null !== $this->info['error']) { - throw new TransportException($this->info['error']); - } - } catch (\Throwable $e) { - $this->close(); - $this->multi->handlesActivity[$this->id] = [new FirstChunk()]; - $this->multi->handlesActivity[$this->id][] = null; - $this->multi->handlesActivity[$this->id][] = $e; - - return; - } - $this->multi->handlesActivity[$this->id] = [new FirstChunk()]; if ('HEAD' === $context['http']['method'] || \in_array($this->info['http_code'], [204, 304], true)) { @@ -209,7 +176,7 @@ private function open(): void return; } - $this->multi->openHandles[$this->id] = [$h, $this->buffer, $this->inflate, $this->content, $this->onProgress, &$this->remaining, &$this->info]; + $this->multi->openHandles[$this->id] = [$h, $this->buffer, $this->onProgress, &$this->remaining, &$this->info]; } /** @@ -230,11 +197,7 @@ private static function schedule(self $response, array &$runningResponses): void $runningResponses[$i] = [$response->multi, []]; } - if (null === $response->remaining) { - $response->multi->pendingResponses[] = $response; - } else { - $runningResponses[$i][1][$response->id] = $response; - } + $runningResponses[$i][1][$response->id] = $response; if (null === $response->buffer) { // Response already completed @@ -253,15 +216,15 @@ private static function perform(NativeClientState $multi, array &$responses = nu $multi->handles = []; } - foreach ($multi->openHandles as $i => [$h, $buffer, $inflate, $content, $onProgress]) { + foreach ($multi->openHandles as $i => [$h, $buffer, $onProgress]) { $hasActivity = false; - $remaining = &$multi->openHandles[$i][5]; - $info = &$multi->openHandles[$i][6]; + $remaining = &$multi->openHandles[$i][3]; + $info = &$multi->openHandles[$i][4]; $e = null; // Read incoming buffer and write it to the dechunk one try { - while ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) { + if ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) { fwrite($buffer, $data); $hasActivity = true; $multi->sleep = false; @@ -289,16 +252,8 @@ private static function perform(NativeClientState $multi, array &$responses = nu rewind($buffer); ftruncate($buffer, 0); - if (null !== $inflate && false === $data = @inflate_add($inflate, $data)) { - $e = new TransportException('Error while processing content unencoding.'); - } - - if ('' !== $data && null === $e) { + if (null === $e) { $multi->handlesActivity[$i][] = $data; - - if (null !== $content && \strlen($data) !== fwrite($content, $data)) { - $e = new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($data))); - } } } @@ -336,25 +291,30 @@ private static function perform(NativeClientState $multi, array &$responses = nu return; } - if ($multi->pendingResponses && \count($multi->handles) < $multi->maxHostConnections) { - // Open the next pending request - this is a blocking operation so we do only one of them - /** @var self $response */ - $response = array_shift($multi->pendingResponses); - $response->open(); - $responses[$response->id] = $response; - $multi->sleep = false; - self::perform($response->multi); - - if (null !== $response->handle) { - $multi->handles[] = $response->handle; + // Create empty activity lists to tell ResponseTrait::stream() we still have pending requests + foreach ($responses as $i => $response) { + if (null === $response->remaining && null !== $response->buffer) { + $multi->handlesActivity[$i] = []; } } - if ($multi->pendingResponses) { - // Create empty activity list to tell ResponseTrait::stream() we still have pending requests - $response = $multi->pendingResponses[0]; - $responses[$response->id] = $response; - $multi->handlesActivity[$response->id] = []; + if (\count($multi->handles) >= $multi->maxHostConnections) { + return; + } + + // Open the next pending request - this is a blocking operation so we do only one of them + foreach ($responses as $i => $response) { + if (null === $response->remaining && null !== $response->buffer) { + $response->open(); + $multi->sleep = false; + self::perform($multi); + + if (null !== $response->handle) { + $multi->handles[] = $response->handle; + } + + break; + } } } diff --git a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php index 0ea434e3a0da4..b7b576e564e8a 100644 --- a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php +++ b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php @@ -43,11 +43,6 @@ trait ResponseTrait */ private $initializer; - /** - * @var resource A php://temp stream typically - */ - private $content; - private $info = [ 'response_headers' => [], 'http_code' => 0, @@ -59,6 +54,9 @@ trait ResponseTrait private $handle; private $id; private $timeout = 0; + private $inflate; + private $shouldBuffer; + private $content; private $finalInfo; private $offset = 0; private $jsonData; @@ -69,8 +67,7 @@ trait ResponseTrait public function getStatusCode(): int { if ($this->initializer) { - ($this->initializer)($this); - $this->initializer = null; + self::initialize($this); } return $this->info['http_code']; @@ -82,8 +79,7 @@ public function getStatusCode(): int public function getHeaders(bool $throw = true): array { if ($this->initializer) { - ($this->initializer)($this); - $this->initializer = null; + self::initialize($this); } if ($throw) { @@ -99,8 +95,7 @@ public function getHeaders(bool $throw = true): array public function getContent(bool $throw = true): string { if ($this->initializer) { - ($this->initializer)($this); - $this->initializer = null; + self::initialize($this); } if ($throw) { @@ -204,7 +199,11 @@ public function toStream(bool $throw = true) $this->getHeaders($throw); } - return StreamWrapper::createResource($this, null, $this->content, $this->handle && 'stream' === get_resource_type($this->handle) ? $this->handle : null); + $stream = StreamWrapper::createResource($this); + stream_get_meta_data($stream)['wrapper_data'] + ->bindHandles($this->handle, $this->content); + + return $stream; } /** @@ -227,10 +226,34 @@ abstract protected static function perform(ClientState $multi, array &$responses */ abstract protected static function select(ClientState $multi, float $timeout): int; + private static function initialize(self $response): void + { + if (null !== $response->info['error']) { + throw new TransportException($response->info['error']); + } + + try { + if (($response->initializer)($response)) { + foreach (self::stream([$response]) as $chunk) { + if ($chunk->isFirst()) { + break; + } + } + } + } catch (\Throwable $e) { + // Persist timeouts thrown during initialization + $response->info['error'] = $e->getMessage(); + $response->close(); + throw $e; + } + + $response->initializer = null; + } + private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void { foreach ($responseHeaders as $h) { - if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([12345]\d\d) .*#', $h, $m)) { + if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([12345]\d\d)(?: |$)#', $h, $m)) { if ($headers) { $debug .= "< \r\n"; $headers = []; @@ -272,8 +295,7 @@ private function checkStatusCode() private function doDestruct() { if ($this->initializer && null === $this->info['error']) { - ($this->initializer)($this); - $this->initializer = null; + self::initialize($this); $this->checkStatusCode(); } } @@ -325,6 +347,16 @@ public static function stream(iterable $responses, float $timeout = null): \Gene $isTimeout = false; if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) { + if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) { + $multi->handlesActivity[$j] = [null, new TransportException('Error while processing content unencoding.')]; + continue; + } + + if ('' !== $chunk && null !== $response->content && \strlen($chunk) !== fwrite($response->content, $chunk)) { + $multi->handlesActivity[$j] = [null, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($chunk)))]; + continue; + } + $response->offset += \strlen($chunk); $chunk = new DataChunk($response->offset, $chunk); } elseif (null === $chunk) { @@ -352,6 +384,28 @@ public static function stream(iterable $responses, float $timeout = null): \Gene $response->logger->info(sprintf('Response: "%s %s"', $info['http_code'], $info['url'])); } + $response->inflate = \extension_loaded('zlib') && $response->inflate && 'gzip' === ($response->headers['content-encoding'][0] ?? null) ? inflate_init(ZLIB_ENCODING_GZIP) : null; + + if ($response->shouldBuffer instanceof \Closure) { + try { + $response->shouldBuffer = ($response->shouldBuffer)($response->headers); + + if (null !== $response->info['error']) { + throw new TransportException($response->info['error']); + } + } catch (\Throwable $e) { + $response->close(); + $multi->handlesActivity[$j] = [null, $e]; + } + } + + if (true === $response->shouldBuffer) { + $response->content = fopen('php://temp', 'w+'); + } elseif (\is_resource($response->shouldBuffer)) { + $response->content = $response->shouldBuffer; + } + $response->shouldBuffer = null; + yield $response => $chunk; if ($response->initializer && null === $response->info['error']) { diff --git a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php index 59fd118e86e01..105d11671a7b2 100644 --- a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php +++ b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php @@ -37,19 +37,17 @@ class StreamWrapper /** @var resource|null */ private $handle; + private $blocking = true; + private $timeout; private $eof = false; private $offset = 0; /** * Creates a PHP stream resource from a ResponseInterface. * - * @param resource|null $contentBuffer The seekable resource where the response body is buffered - * @param resource|null $selectHandle The resource handle that should be monitored when - * stream_select() is used on the created stream - * * @return resource */ - public static function createResource(ResponseInterface $response, HttpClientInterface $client = null, $contentBuffer = null, $selectHandle = null) + public static function createResource(ResponseInterface $response, HttpClientInterface $client = null) { if (null === $client && !method_exists($response, 'stream')) { throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__)); @@ -63,8 +61,6 @@ public static function createResource(ResponseInterface $response, HttpClientInt $context = [ 'client' => $client ?? $response, 'response' => $response, - 'content' => $contentBuffer, - 'handle' => $selectHandle, ]; return fopen('symfony://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context])) ?: null; @@ -78,6 +74,17 @@ public function getResponse(): ResponseInterface return $this->response; } + /** + * @param resource|null $handle The resource handle that should be monitored when + * stream_select() is used on the created stream + * @param resource|null $content The seekable resource where the response body is buffered + */ + public function bindHandles(&$handle, &$content): void + { + $this->handle = &$handle; + $this->content = &$content; + } + public function stream_open(string $path, string $mode, int $options): bool { if ('r' !== $mode) { @@ -91,8 +98,6 @@ public function stream_open(string $path, string $mode, int $options): bool $context = stream_context_get_options($this->context)['symfony'] ?? null; $this->client = $context['client'] ?? null; $this->response = $context['response'] ?? null; - $this->content = $context['content'] ?? null; - $this->handle = $context['handle'] ?? null; $this->context = null; if (null !== $this->client && null !== $this->response) { @@ -147,7 +152,7 @@ public function stream_read(int $count) return $data; } - foreach ($this->client->stream([$this->response]) as $chunk) { + foreach ($this->client->stream([$this->response], $this->blocking ? $this->timeout : 0) as $chunk) { try { $this->eof = true; $this->eof = !$chunk->isTimeout(); @@ -178,6 +183,19 @@ public function stream_read(int $count) return ''; } + public function stream_set_option(int $option, int $arg1, ?int $arg2): bool + { + if (STREAM_OPTION_BLOCKING === $option) { + $this->blocking = (bool) $arg1; + } elseif (STREAM_OPTION_READ_TIMEOUT === $option) { + $this->timeout = $arg1 + $arg2 / 1e6; + } else { + return false; + } + + return true; + } + public function stream_tell(): int { return $this->offset; @@ -238,6 +256,8 @@ public function stream_seek(int $offset, int $whence = SEEK_SET): bool public function stream_cast(int $castAs) { if (STREAM_CAST_FOR_SELECT === $castAs) { + $this->response->getHeaders(false); + return $this->handle ?? false; } diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php index b4b5b9ec1a639..29171969b457e 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php +++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php @@ -75,4 +75,20 @@ public function testToStream404() $response = $client->request('GET', 'http://localhost:8057/404'); $stream = $response->toStream(); } + + public function testNonBlockingStream() + { + $client = $this->getHttpClient(__FUNCTION__); + $response = $client->request('GET', 'http://localhost:8057/timeout-body'); + $stream = $response->toStream(); + + $this->assertTrue(stream_set_blocking($stream, false)); + $this->assertSame('<1>', fread($stream, 8192)); + $this->assertFalse(feof($stream)); + + $this->assertTrue(stream_set_blocking($stream, true)); + $this->assertSame('<2>', fread($stream, 8192)); + $this->assertSame('', fread($stream, 8192)); + $this->assertTrue(feof($stream)); + } } diff --git a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php index 7bc528354811b..bce4bfafea8cc 100644 --- a/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php @@ -171,6 +171,10 @@ protected function getHttpClient(string $testCase): HttpClientInterface return $client; + case 'testNonBlockingStream': + $responses[] = new MockResponse((function () { yield '<1>'; yield ''; yield '<2>'; })(), ['response_headers' => $headers]); + break; + case 'testMaxDuration': $mock = $this->getMockBuilder(ResponseInterface::class)->getMock(); $mock->expects($this->any()) diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index d37cbd29681c4..30eff944d750a 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -214,7 +214,7 @@ public function prepare(Request $request) } if ('x-accel-redirect' === strtolower($type)) { // Do X-Accel-Mapping substitutions. - // @link http://wiki.nginx.org/X-accel#X-Accel-Redirect + // @link https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/#x-accel-redirect $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping', ''), ',='); foreach ($parts as $part) { list($pathPrefix, $location) = $part; diff --git a/src/Symfony/Component/HttpFoundation/LICENSE b/src/Symfony/Component/HttpFoundation/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/HttpFoundation/LICENSE +++ b/src/Symfony/Component/HttpFoundation/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/HttpFoundation/README.md b/src/Symfony/Component/HttpFoundation/README.md index 8907f0b967896..ac98f9b80ad5d 100644 --- a/src/Symfony/Component/HttpFoundation/README.md +++ b/src/Symfony/Component/HttpFoundation/README.md @@ -7,7 +7,7 @@ specification. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/http_foundation/index.html) + * [Documentation](https://symfony.com/doc/current/components/http_foundation.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 698af72ce2b2e..443d77d6724a5 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -350,6 +350,10 @@ protected function lookup(Request $request, bool $catch = false) return $this->validate($request, $entry, $catch); } + if ($entry->headers->hasCacheControlDirective('no-cache')) { + return $this->validate($request, $entry, $catch); + } + $this->record($request, 'fresh'); $entry->headers->set('Age', $entry->getAge()); diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 2394fac7b0f4d..944c69ca884f2 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -68,11 +68,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - const VERSION = '5.0.2'; - const VERSION_ID = 50002; + const VERSION = '5.0.3'; + const VERSION_ID = 50003; const MAJOR_VERSION = 5; const MINOR_VERSION = 0; - const RELEASE_VERSION = 2; + const RELEASE_VERSION = 3; const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '07/2020'; @@ -475,7 +475,7 @@ public function write(string $content, array $metadata = null) } } - public function __destruct() + public function release() { flock($this->lock, LOCK_UN); fclose($this->lock); @@ -553,7 +553,7 @@ public function __destruct() } $this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass()); - unset($cache); + $cache->release(); $this->container = require $cachePath; $this->container->set('kernel', $this); diff --git a/src/Symfony/Component/HttpKernel/LICENSE b/src/Symfony/Component/HttpKernel/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/HttpKernel/LICENSE +++ b/src/Symfony/Component/HttpKernel/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/HttpKernel/README.md b/src/Symfony/Component/HttpKernel/README.md index cc5e74b6bca3b..abdaf513f9cde 100644 --- a/src/Symfony/Component/HttpKernel/README.md +++ b/src/Symfony/Component/HttpKernel/README.md @@ -9,7 +9,7 @@ an advanced CMS system (Drupal). Resources --------- - * [Documentation](https://symfony.com/doc/current/components/http_kernel/index.html) + * [Documentation](https://symfony.com/doc/current/components/http_kernel.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/DisallowRobotsIndexingListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/DisallowRobotsIndexingListenerTest.php index 53b317b7611ce..6534ebf4e2122 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/DisallowRobotsIndexingListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/DisallowRobotsIndexingListenerTest.php @@ -24,8 +24,9 @@ class DisallowRobotsIndexingListenerTest extends TestCase /** * @dataProvider provideResponses */ - public function testInvoke(?string $expected, Response $response): void + public function testInvoke(?string $expected, array $responseArgs) { + $response = new Response(...$responseArgs); $listener = new DisallowRobotsIndexingListener(); $event = new ResponseEvent($this->createMock(HttpKernelInterface::class), $this->createMock(Request::class), KernelInterface::MASTER_REQUEST, $response); @@ -37,11 +38,11 @@ public function testInvoke(?string $expected, Response $response): void public function provideResponses(): iterable { - yield 'No header' => ['noindex', new Response()]; + yield 'No header' => ['noindex', []]; yield 'Header already set' => [ 'something else', - new Response('', 204, ['X-Robots-Tag' => 'something else']), + ['', 204, ['X-Robots-Tag' => 'something else']], ]; } } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php index aa2c611455a3c..34f1dc7f3b512 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php @@ -443,6 +443,22 @@ public function testCachesResponsesWithExplicitNoCacheDirective() $this->assertTrue($this->response->headers->has('Age')); } + public function testRevalidatesResponsesWithNoCacheDirectiveEvenIfFresh() + { + $this->setNextResponse(200, ['Cache-Control' => 'public, no-cache, max-age=10', 'ETag' => 'some-etag'], 'OK'); + $this->request('GET', '/'); // warm the cache + + sleep(5); + + $this->setNextResponse(304, ['Cache-Control' => 'public, no-cache, max-age=10', 'ETag' => 'some-etag']); + $this->request('GET', '/'); + + $this->assertHttpKernelIsCalled(); // no-cache -> MUST have revalidated at origin + $this->assertTraceContains('valid'); + $this->assertEquals('OK', $this->response->getContent()); + $this->assertEquals(0, $this->response->getAge()); + } + public function testCachesResponsesWithAnExpirationHeader() { $time = \DateTime::createFromFormat('U', time() + 5); diff --git a/src/Symfony/Component/Inflector/LICENSE b/src/Symfony/Component/Inflector/LICENSE index f03153cc4a232..2749b15672205 100644 --- a/src/Symfony/Component/Inflector/LICENSE +++ b/src/Symfony/Component/Inflector/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2012-2019 Fabien Potencier +Copyright (c) 2012-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Intl/LICENSE b/src/Symfony/Component/Intl/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Intl/LICENSE +++ b/src/Symfony/Component/Intl/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Ldap/LICENSE b/src/Symfony/Component/Ldap/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Ldap/LICENSE +++ b/src/Symfony/Component/Ldap/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Lock/LICENSE b/src/Symfony/Component/Lock/LICENSE index 3c464ca94359b..a7ec70801827a 100644 --- a/src/Symfony/Component/Lock/LICENSE +++ b/src/Symfony/Component/Lock/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2016-2019 Fabien Potencier +Copyright (c) 2016-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Mailer/Bridge/Amazon/LICENSE b/src/Symfony/Component/Mailer/Bridge/Amazon/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Mailer/Bridge/Amazon/LICENSE +++ b/src/Symfony/Component/Mailer/Bridge/Amazon/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Mailer/Bridge/Google/LICENSE b/src/Symfony/Component/Mailer/Bridge/Google/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Mailer/Bridge/Google/LICENSE +++ b/src/Symfony/Component/Mailer/Bridge/Google/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/LICENSE b/src/Symfony/Component/Mailer/Bridge/Mailchimp/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/LICENSE +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillApiTransportTest.php index 2bec482818421..af4cdbeebedfb 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Tests/Transport/MandrillApiTransportTest.php @@ -13,6 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillApiTransport; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; class MandrillApiTransportTest extends TestCase { @@ -41,4 +44,21 @@ public function getTransportData() ], ]; } + + public function testCustomHeader() + { + $email = new Email(); + $email->getHeaders()->addTextHeader('foo', 'bar'); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new MandrillApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(MandrillApiTransport::class, 'getPayload'); + $method->setAccessible(true); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('message', $payload); + $this->assertArrayHasKey('headers', $payload['message']); + $this->assertCount(1, $payload['message']['headers']); + $this->assertEquals('foo: bar', $payload['message']['headers'][0]); + } } diff --git a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php index 50fc33baaa65c..af221ca66e4fb 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php @@ -94,10 +94,14 @@ private function getPayload(Email $email, Envelope $envelope): array 'type' => $headers->get('Content-Type')->getBody(), ]; + if ($name = $headers->getHeaderParameter('Content-Disposition', 'name')) { + $att['name'] = $name; + } + if ('inline' === $disposition) { - $payload['images'][] = $att; + $payload['message']['images'][] = $att; } else { - $payload['attachments'][] = $att; + $payload['message']['attachments'][] = $att; } } @@ -107,7 +111,7 @@ private function getPayload(Email $email, Envelope $envelope): array continue; } - $payload['message']['headers'][] = $name.': '.$header->toString(); + $payload['message']['headers'][] = $name.': '.$header->getBodyAsString(); } return $payload; diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/LICENSE b/src/Symfony/Component/Mailer/Bridge/Mailgun/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailgun/LICENSE +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php index eb9838390aed8..f30fa0285c059 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Tests/Transport/MailgunApiTransportTest.php @@ -13,6 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunApiTransport; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; class MailgunApiTransportTest extends TestCase { @@ -45,4 +48,20 @@ public function getTransportData() ], ]; } + + public function testCustomHeader() + { + $json = json_encode(['foo' => 'bar']); + $email = new Email(); + $email->getHeaders()->addTextHeader('X-Mailgun-Variables', $json); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new MailgunApiTransport('ACCESS_KEY', 'DOMAIN'); + $method = new \ReflectionMethod(MailgunApiTransport::class, 'getPayload'); + $method->setAccessible(true); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('h:x-mailgun-variables', $payload); + $this->assertEquals($json, $payload['h:x-mailgun-variables']); + } } diff --git a/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php index 8d7b5cc7e28be..cf40a8cf1eaa6 100644 --- a/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Mailgun/Transport/MailgunApiTransport.php @@ -114,7 +114,7 @@ private function getPayload(Email $email, Envelope $envelope): array continue; } - $payload['h:'.$name] = $header->toString(); + $payload['h:'.$name] = $header->getBodyAsString(); } return $payload; diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/LICENSE b/src/Symfony/Component/Mailer/Bridge/Postmark/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/LICENSE +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php index b6568706f8306..6996997b659c2 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Tests/Transport/PostmarkApiTransportTest.php @@ -13,6 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkApiTransport; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; class PostmarkApiTransportTest extends TestCase { @@ -41,4 +44,21 @@ public function getTransportData() ], ]; } + + public function testCustomHeader() + { + $email = new Email(); + $email->getHeaders()->addTextHeader('foo', 'bar'); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new PostmarkApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(PostmarkApiTransport::class, 'getPayload'); + $method->setAccessible(true); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('Headers', $payload); + $this->assertCount(1, $payload['Headers']); + + $this->assertEquals(['Name' => 'foo', 'Value' => 'bar'], $payload['Headers'][0]); + } } diff --git a/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php index 96dd8d4a65f3f..610f858260549 100644 --- a/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Postmark/Transport/PostmarkApiTransport.php @@ -84,7 +84,7 @@ private function getPayload(Email $email, Envelope $envelope): array $payload['Headers'][] = [ 'Name' => $name, - 'Value' => $header->toString(), + 'Value' => $header->getBodyAsString(), ]; } diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/LICENSE b/src/Symfony/Component/Mailer/Bridge/Sendgrid/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/LICENSE +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php index 9ad5c280ecbad..ab271d574c796 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Tests/Transport/SendgridApiTransportTest.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridApiTransport; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\ResponseInterface; @@ -48,8 +50,8 @@ public function getTransportData() public function testSend() { $email = new Email(); - $email->from('foo@example.com') - ->to('bar@example.com') + $email->from(new Address('foo@example.com', 'Ms. Foo Bar')) + ->to(new Address('bar@example.com', 'Mr. Recipient')) ->bcc('baz@example.com') ->text('content'); @@ -73,12 +75,18 @@ public function testSend() 'json' => [ 'personalizations' => [ [ - 'to' => [['email' => 'bar@example.com']], + 'to' => [[ + 'email' => 'bar@example.com', + 'name' => 'Mr. Recipient', + ]], 'subject' => null, 'bcc' => [['email' => 'baz@example.com']], ], ], - 'from' => ['email' => 'foo@example.com'], + 'from' => [ + 'email' => 'foo@example.com', + 'name' => 'Ms. Foo Bar', + ], 'content' => [ ['type' => 'text/plain', 'value' => 'content'], ], @@ -90,4 +98,72 @@ public function testSend() $mailer = new SendgridApiTransport('foo', $httpClient); $mailer->send($email); } + + public function testLineBreaksInEncodedAttachment() + { + $email = new Email(); + $email->from('foo@example.com') + ->to('bar@example.com') + // even if content doesn't include new lines, the base64 encoding performed later may add them + ->attach('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod', 'lorem.txt'); + + $response = $this->createMock(ResponseInterface::class); + + $response + ->expects($this->once()) + ->method('getStatusCode') + ->willReturn(202); + $response + ->expects($this->once()) + ->method('getHeaders') + ->willReturn(['x-message-id' => '1']); + + $httpClient = $this->createMock(HttpClientInterface::class); + + $httpClient + ->expects($this->once()) + ->method('request') + ->with('POST', 'https://api.sendgrid.com/v3/mail/send', [ + 'json' => [ + 'personalizations' => [ + [ + 'to' => [['email' => 'bar@example.com']], + 'subject' => null, + ], + ], + 'from' => ['email' => 'foo@example.com'], + 'content' => [], + 'attachments' => [ + [ + 'content' => 'TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2NpbmcgZWxpdCwgc2VkIGRvIGVpdXNtb2Q=', + 'filename' => 'lorem.txt', + 'type' => 'application/octet-stream', + 'disposition' => 'attachment', + ], + ], + ], + 'auth_bearer' => 'foo', + ]) + ->willReturn($response); + + $mailer = new SendgridApiTransport('foo', $httpClient); + + $mailer->send($email); + } + + public function testCustomHeader() + { + $email = new Email(); + $email->getHeaders()->addTextHeader('foo', 'bar'); + $envelope = new Envelope(new Address('alice@system.com'), [new Address('bob@system.com')]); + + $transport = new SendgridApiTransport('ACCESS_KEY'); + $method = new \ReflectionMethod(SendgridApiTransport::class, 'getPayload'); + $method->setAccessible(true); + $payload = $method->invoke($transport, $email, $envelope); + + $this->assertArrayHasKey('headers', $payload); + $this->assertArrayHasKey('foo', $payload['headers']); + $this->assertEquals('bar', $payload['headers']['foo']); + } } diff --git a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php index 262983afd73fd..df90d2147277a 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php +++ b/src/Symfony/Component/Mailer/Bridge/Sendgrid/Transport/SendgridApiTransport.php @@ -63,11 +63,19 @@ protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $e private function getPayload(Email $email, Envelope $envelope): array { - $addressStringifier = function (Address $address) {return ['email' => $address->toString()]; }; + $addressStringifier = function (Address $address) { + $stringified = ['email' => $address->getAddress()]; + + if ($address->getName()) { + $stringified['name'] = $address->getName(); + } + + return $stringified; + }; $payload = [ 'personalizations' => [], - 'from' => ['email' => $envelope->getSender()->toString()], + 'from' => $addressStringifier($envelope->getSender()), 'content' => $this->getContent($email), ]; @@ -96,7 +104,7 @@ private function getPayload(Email $email, Envelope $envelope): array continue; } - $payload['headers'][$name] = $header->toString(); + $payload['headers'][$name] = $header->getBodyAsString(); } return $payload; @@ -124,7 +132,7 @@ private function getAttachments(Email $email): array $disposition = $headers->getHeaderBody('Content-Disposition'); $att = [ - 'content' => $attachment->bodyToString(), + 'content' => str_replace("\r\n", '', $attachment->bodyToString()), 'type' => $headers->get('Content-Type')->getBody(), 'filename' => $filename, 'disposition' => $disposition, diff --git a/src/Symfony/Component/Mailer/LICENSE b/src/Symfony/Component/Mailer/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Mailer/LICENSE +++ b/src/Symfony/Component/Mailer/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Messenger/Command/DebugCommand.php b/src/Symfony/Component/Messenger/Command/DebugCommand.php index 2c8c546c23509..b86dc4b547d87 100644 --- a/src/Symfony/Component/Messenger/Command/DebugCommand.php +++ b/src/Symfony/Component/Messenger/Command/DebugCommand.php @@ -108,9 +108,9 @@ private function formatConditions(array $options): string $optionsMapping = []; foreach ($options as $key => $value) { - $optionsMapping[] = ' '.$key.'='.$value; + $optionsMapping[] = $key.'='.$value; } - return ' (when'.implode(', ', $optionsMapping).')'; + return ' (when '.implode(', ', $optionsMapping).')'; } } diff --git a/src/Symfony/Component/Messenger/LICENSE b/src/Symfony/Component/Messenger/LICENSE index 3f853aaf35fe1..69d925ba7511e 100644 --- a/src/Symfony/Component/Messenger/LICENSE +++ b/src/Symfony/Component/Messenger/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 Fabien Potencier +Copyright (c) 2018-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Messenger/README.md b/src/Symfony/Component/Messenger/README.md index 2fff6a15578b1..f40945b37aa45 100644 --- a/src/Symfony/Component/Messenger/README.md +++ b/src/Symfony/Component/Messenger/README.md @@ -7,6 +7,7 @@ message queues. Resources --------- + * [Documentation](https://symfony.com/doc/current/components/messenger.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php index 02812adb47a1e..e637d51dd3126 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php @@ -40,7 +40,7 @@ public function testOutput() { $command = new DebugCommand([ 'command_bus' => [ - DummyCommand::class => [[DummyCommandHandler::class, []]], + DummyCommand::class => [[DummyCommandHandler::class, ['option1' => '1', 'option2' => '2']]], MultipleBusesMessage::class => [[MultipleBusesMessageHandler::class, []]], ], 'query_bus' => [ @@ -62,12 +62,12 @@ public function testOutput() The following messages can be dispatched: - --------------------------------------------------------------------------------------- - Symfony\Component\Messenger\Tests\Fixtures\DummyCommand - handled by Symfony\Component\Messenger\Tests\Fixtures\DummyCommandHandler - Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessage - handled by Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessageHandler - --------------------------------------------------------------------------------------- + ----------------------------------------------------------------------------------------------------------- + Symfony\Component\Messenger\Tests\Fixtures\DummyCommand + handled by Symfony\Component\Messenger\Tests\Fixtures\DummyCommandHandler (when option1=1, option2=2) + Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessage + handled by Symfony\Component\Messenger\Tests\Fixtures\MultipleBusesMessageHandler + ----------------------------------------------------------------------------------------------------------- query_bus --------- diff --git a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php b/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php index 57eafa2c1db77..0be034dd3de3d 100644 --- a/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php +++ b/src/Symfony/Component/Messenger/Tests/Transport/RedisExt/ConnectionTest.php @@ -186,6 +186,18 @@ public function testGetNonBlocking() $redis->del('messenger-getnonblocking'); } + public function testJsonError() + { + $redis = new \Redis(); + $connection = Connection::fromDsn('redis://localhost/json-error', [], $redis); + try { + $connection->add("\xB1\x31", []); + } catch (TransportException $e) { + } + + $this->assertSame('Malformed UTF-8 characters, possibly incorrectly encoded', $e->getMessage()); + } + public function testMaxEntries() { $redis = $this->getMockBuilder(\Redis::class)->disableOriginalConstructor()->getMock(); diff --git a/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php b/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php index 33d6057f67004..e1980221625e3 100644 --- a/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php +++ b/src/Symfony/Component/Messenger/Transport/RedisExt/Connection.php @@ -248,6 +248,10 @@ public function add(string $body, array $headers, int $delayInMs = 0): void 'uniqid' => uniqid('', true), ]); + if (false === $message) { + throw new TransportException(json_last_error_msg()); + } + $score = (int) ($this->getCurrentTimeInMilliseconds() + $delayInMs); $added = $this->connection->zadd($this->queue, ['NX'], $score, $message); } else { @@ -256,6 +260,10 @@ public function add(string $body, array $headers, int $delayInMs = 0): void 'headers' => $headers, ]); + if (false === $message) { + throw new TransportException(json_last_error_msg()); + } + if ($this->maxEntries) { $added = $this->connection->xadd($this->stream, '*', ['message' => $message], $this->maxEntries, true); } else { diff --git a/src/Symfony/Component/Mime/LICENSE b/src/Symfony/Component/Mime/LICENSE index 9a9a61b1c6c10..d53be68356dbf 100644 --- a/src/Symfony/Component/Mime/LICENSE +++ b/src/Symfony/Component/Mime/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010-2019 Fabien Potencier +Copyright (c) 2010-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Notifier/Bridge/Nexmo/LICENSE b/src/Symfony/Component/Notifier/Bridge/Nexmo/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Notifier/Bridge/Nexmo/LICENSE +++ b/src/Symfony/Component/Notifier/Bridge/Nexmo/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Notifier/Bridge/Nexmo/NexmoTransport.php b/src/Symfony/Component/Notifier/Bridge/Nexmo/NexmoTransport.php index 9ce02fa0b703a..ae46ffbea2fe3 100644 --- a/src/Symfony/Component/Notifier/Bridge/Nexmo/NexmoTransport.php +++ b/src/Symfony/Component/Notifier/Bridge/Nexmo/NexmoTransport.php @@ -69,7 +69,7 @@ protected function doSend(MessageInterface $message): void $result = $response->toArray(false); foreach ($result['messages'] as $msg) { - if ($msg['status'] ?? 0 > 0) { + if ($msg['status'] ?? false) { throw new TransportException(sprintf('Unable to send the SMS: %s (%s).', $msg['error-text'], $msg['status']), $response); } } diff --git a/src/Symfony/Component/Notifier/Bridge/Nexmo/NexmoTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Nexmo/NexmoTransportFactory.php index 21f2e080bd8e4..e4a4479fb2f1e 100644 --- a/src/Symfony/Component/Notifier/Bridge/Nexmo/NexmoTransportFactory.php +++ b/src/Symfony/Component/Notifier/Bridge/Nexmo/NexmoTransportFactory.php @@ -23,6 +23,9 @@ */ final class NexmoTransportFactory extends AbstractTransportFactory { + /** + * @return NexmoTransport + */ public function create(Dsn $dsn): TransportInterface { $scheme = $dsn->getScheme(); diff --git a/src/Symfony/Component/Notifier/Bridge/Nexmo/composer.json b/src/Symfony/Component/Notifier/Bridge/Nexmo/composer.json index cfcc1bcf18058..019d06fb269c1 100644 --- a/src/Symfony/Component/Notifier/Bridge/Nexmo/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Nexmo/composer.json @@ -21,7 +21,7 @@ "symfony/notifier": "~5.0.0" }, "autoload": { - "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Twilio\\": "" }, + "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Nexmo\\": "" }, "exclude-from-classmap": [ "/Tests/" ] diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/LICENSE b/src/Symfony/Component/Notifier/Bridge/Slack/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Notifier/Bridge/Slack/LICENSE +++ b/src/Symfony/Component/Notifier/Bridge/Slack/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Notifier/Bridge/Slack/SlackTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Slack/SlackTransportFactory.php index 6e12c0b218a63..0600b1c9104ae 100644 --- a/src/Symfony/Component/Notifier/Bridge/Slack/SlackTransportFactory.php +++ b/src/Symfony/Component/Notifier/Bridge/Slack/SlackTransportFactory.php @@ -23,6 +23,9 @@ */ final class SlackTransportFactory extends AbstractTransportFactory { + /** + * @return SlackTransport + */ public function create(Dsn $dsn): TransportInterface { $scheme = $dsn->getScheme(); diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/LICENSE b/src/Symfony/Component/Notifier/Bridge/Telegram/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Notifier/Bridge/Telegram/LICENSE +++ b/src/Symfony/Component/Notifier/Bridge/Telegram/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Notifier/Bridge/Telegram/TelegramTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Telegram/TelegramTransportFactory.php index 07378dc6a8cd5..9640d64c3c490 100644 --- a/src/Symfony/Component/Notifier/Bridge/Telegram/TelegramTransportFactory.php +++ b/src/Symfony/Component/Notifier/Bridge/Telegram/TelegramTransportFactory.php @@ -24,6 +24,9 @@ */ final class TelegramTransportFactory extends AbstractTransportFactory { + /** + * @return TelegramTransport + */ public function create(Dsn $dsn): TransportInterface { $scheme = $dsn->getScheme(); diff --git a/src/Symfony/Component/Notifier/Bridge/Twilio/LICENSE b/src/Symfony/Component/Notifier/Bridge/Twilio/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Notifier/Bridge/Twilio/LICENSE +++ b/src/Symfony/Component/Notifier/Bridge/Twilio/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Notifier/Bridge/Twilio/TwilioTransportFactory.php b/src/Symfony/Component/Notifier/Bridge/Twilio/TwilioTransportFactory.php index 8353eec7e6810..65fb413a3dc8f 100644 --- a/src/Symfony/Component/Notifier/Bridge/Twilio/TwilioTransportFactory.php +++ b/src/Symfony/Component/Notifier/Bridge/Twilio/TwilioTransportFactory.php @@ -23,6 +23,9 @@ */ final class TwilioTransportFactory extends AbstractTransportFactory { + /** + * @return TwilioTransport + */ public function create(Dsn $dsn): TransportInterface { $scheme = $dsn->getScheme(); diff --git a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php index 31e5eed341e5e..c61446339ae5c 100644 --- a/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php +++ b/src/Symfony/Component/Notifier/Exception/UnsupportedSchemeException.php @@ -40,6 +40,9 @@ class UnsupportedSchemeException extends LogicException ], ]; + /** + * @param string[] $supported + */ public function __construct(Dsn $dsn, string $name = null, array $supported = []) { $provider = $dsn->getScheme(); diff --git a/src/Symfony/Component/Notifier/LICENSE b/src/Symfony/Component/Notifier/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/Notifier/LICENSE +++ b/src/Symfony/Component/Notifier/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Notifier/Message/SmsMessage.php b/src/Symfony/Component/Notifier/Message/SmsMessage.php index e704e169a9040..6b0bdd3806df4 100644 --- a/src/Symfony/Component/Notifier/Message/SmsMessage.php +++ b/src/Symfony/Component/Notifier/Message/SmsMessage.php @@ -37,7 +37,7 @@ public function __construct(string $phone, string $subject) public static function fromNotification(Notification $notification, Recipient $recipient, string $transport = null): self { if (!$recipient instanceof SmsRecipientInterface) { - throw new LogicException(sprintf('To send a SMS message, "%s" should implement "%s" or the recipient should implement "%s".', get_class($notification), SmsNotificationInterface::class, SmsRecipientInterface::class)); + throw new LogicException(sprintf('To send a SMS message, "%s" should implement "%s" or the recipient should implement "%s".', \get_class($notification), SmsNotificationInterface::class, SmsRecipientInterface::class)); } return new self($recipient->getPhone(), $notification->getSubject()); @@ -58,7 +58,7 @@ public function getPhone(): string return $this->phone; } - public function getRecipientId(): ?string + public function getRecipientId(): string { return $this->phone; } diff --git a/src/Symfony/Component/Notifier/Transport/AbstractTransportFactory.php b/src/Symfony/Component/Notifier/Transport/AbstractTransportFactory.php index be92b3c57883b..2b0cae4b20c11 100644 --- a/src/Symfony/Component/Notifier/Transport/AbstractTransportFactory.php +++ b/src/Symfony/Component/Notifier/Transport/AbstractTransportFactory.php @@ -39,6 +39,9 @@ public function supports(Dsn $dsn): bool return \in_array($dsn->getScheme(), $this->getSupportedSchemes()); } + /** + * @return string[] + */ abstract protected function getSupportedSchemes(): array; protected function getUser(Dsn $dsn): string diff --git a/src/Symfony/Component/Notifier/Transport/NullTransportFactory.php b/src/Symfony/Component/Notifier/Transport/NullTransportFactory.php index f93ff8c7b0c29..abfcd1c75d3f7 100644 --- a/src/Symfony/Component/Notifier/Transport/NullTransportFactory.php +++ b/src/Symfony/Component/Notifier/Transport/NullTransportFactory.php @@ -20,6 +20,9 @@ */ final class NullTransportFactory extends AbstractTransportFactory { + /** + * @return NullTransport + */ public function create(Dsn $dsn): TransportInterface { if ('null' === $dsn->getScheme()) { diff --git a/src/Symfony/Component/OptionsResolver/LICENSE b/src/Symfony/Component/OptionsResolver/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/OptionsResolver/LICENSE +++ b/src/Symfony/Component/OptionsResolver/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Process/LICENSE b/src/Symfony/Component/Process/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Process/LICENSE +++ b/src/Symfony/Component/Process/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 0091ea4f5ccb6..69ccc08489255 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -135,7 +135,7 @@ class Process implements \IteratorAggregate * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input * @param int|float|null $timeout The timeout in seconds or null to disable * - * @throws RuntimeException When proc_open is not installed + * @throws LogicException When proc_open is not installed */ public function __construct(array $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) { @@ -184,7 +184,7 @@ public function __construct(array $command, string $cwd = null, array $env = nul * * @return static * - * @throws RuntimeException When proc_open is not installed + * @throws LogicException When proc_open is not installed */ public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) { @@ -219,9 +219,11 @@ public function __clone() * * @return int The exit status code * - * @throws RuntimeException When process can't be launched - * @throws RuntimeException When process stopped after receiving signal - * @throws LogicException In case a callback is provided and output has been disabled + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled * * @final */ @@ -389,9 +391,9 @@ public function restart(callable $callback = null, array $env = []): self * * @return int The exitcode of the process * - * @throws RuntimeException When process timed out - * @throws RuntimeException When process stopped after receiving signal - * @throws LogicException When process is not yet started + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException When process is not yet started */ public function wait(callable $callback = null) { @@ -402,7 +404,7 @@ public function wait(callable $callback = null) if (null !== $callback) { if (!$this->processPipes->haveReadSupport()) { $this->stop(0); - throw new \LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait"'); + throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait"'); } $this->callback = $this->buildCallback($callback); } @@ -432,8 +434,9 @@ public function wait(callable $callback = null) * from the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * - * @throws RuntimeException When process timed out - * @throws LogicException When process is not yet started + * @throws RuntimeException When process timed out + * @throws LogicException When process is not yet started + * @throws ProcessTimedOutException In case the timeout was reached */ public function waitUntil(callable $callback): bool { @@ -442,7 +445,7 @@ public function waitUntil(callable $callback): bool if (!$this->processPipes->haveReadSupport()) { $this->stop(0); - throw new \LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".'); + throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".'); } $callback = $this->buildCallback($callback); diff --git a/src/Symfony/Component/PropertyAccess/LICENSE b/src/Symfony/Component/PropertyAccess/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/PropertyAccess/LICENSE +++ b/src/Symfony/Component/PropertyAccess/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/PropertyAccess/README.md b/src/Symfony/Component/PropertyAccess/README.md index 1959fd9e93492..891528d29cb94 100644 --- a/src/Symfony/Component/PropertyAccess/README.md +++ b/src/Symfony/Component/PropertyAccess/README.md @@ -7,7 +7,7 @@ object or array using a simple string notation. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/property_access/index.html) + * [Documentation](https://symfony.com/doc/current/components/property_access.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php index e5f2d4f0d507c..97a106a616a6d 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php @@ -201,6 +201,8 @@ private function getDocBlockFromProperty(string $class, string $property): ?DocB return $this->docBlockFactory->create($reflectionProperty, $this->createFromReflector($reflectionProperty->getDeclaringClass())); } catch (\InvalidArgumentException $e) { return null; + } catch (\RuntimeException $e) { + return null; } } @@ -237,6 +239,8 @@ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, i return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflectionMethod->getDeclaringClass())), $prefix]; } catch (\InvalidArgumentException $e) { return null; + } catch (\RuntimeException $e) { + return null; } } diff --git a/src/Symfony/Component/PropertyInfo/LICENSE b/src/Symfony/Component/PropertyInfo/LICENSE index 4cd8bdd3007da..5612f967a232e 100644 --- a/src/Symfony/Component/PropertyInfo/LICENSE +++ b/src/Symfony/Component/PropertyInfo/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2019 Fabien Potencier +Copyright (c) 2015-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Routing/Generator/CompiledUrlGenerator.php b/src/Symfony/Component/Routing/Generator/CompiledUrlGenerator.php index 8c489f8d4c028..b10edce4e9249 100644 --- a/src/Symfony/Component/Routing/Generator/CompiledUrlGenerator.php +++ b/src/Symfony/Component/Routing/Generator/CompiledUrlGenerator.php @@ -40,7 +40,6 @@ public function generate(string $name, array $parameters = [], int $referenceTyp if (null !== $locale) { do { if (($this->compiledRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) { - unset($parameters['_locale']); $name .= '.'.$locale; break; } @@ -53,6 +52,14 @@ public function generate(string $name, array $parameters = [], int $referenceTyp list($variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes) = $this->compiledRoutes[$name]; + if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) { + if (!\in_array('_locale', $variables, true)) { + unset($parameters['_locale']); + } elseif (!isset($parameters['_locale'])) { + $parameters['_locale'] = $defaults['_locale']; + } + } + return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes); } } diff --git a/src/Symfony/Component/Routing/Generator/UrlGenerator.php b/src/Symfony/Component/Routing/Generator/UrlGenerator.php index 504dd321fd147..5473e856e1105 100644 --- a/src/Symfony/Component/Routing/Generator/UrlGenerator.php +++ b/src/Symfony/Component/Routing/Generator/UrlGenerator.php @@ -134,7 +134,6 @@ public function generate(string $name, array $parameters = [], int $referenceTyp if (null !== $locale) { do { if (null !== ($route = $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) { - unset($parameters['_locale']); break; } } while (false !== $locale = strstr($locale, '_', true)); @@ -147,7 +146,18 @@ public function generate(string $name, array $parameters = [], int $referenceTyp // the Route has a cache of its own and is not recompiled as long as it does not get modified $compiledRoute = $route->compile(); - return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes()); + $defaults = $route->getDefaults(); + $variables = $compiledRoute->getVariables(); + + if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) { + if (!\in_array('_locale', $variables, true)) { + unset($parameters['_locale']); + } elseif (!isset($parameters['_locale'])) { + $parameters['_locale'] = $defaults['_locale']; + } + } + + return $this->doGenerate($variables, $defaults, $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes()); } /** diff --git a/src/Symfony/Component/Routing/LICENSE b/src/Symfony/Component/Routing/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Routing/LICENSE +++ b/src/Symfony/Component/Routing/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Routing/README.md b/src/Symfony/Component/Routing/README.md index 88fb1fde5a7bd..a16d9d7fcbbbb 100644 --- a/src/Symfony/Component/Routing/README.md +++ b/src/Symfony/Component/Routing/README.md @@ -6,7 +6,7 @@ The Routing component maps an HTTP request to a set of configuration variables. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/routing/index.html) + * [Documentation](https://symfony.com/doc/current/components/routing.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php b/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php index 521f0f126cda7..de4776ff78331 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/Dumper/CompiledUrlGeneratorDumperTest.php @@ -131,9 +131,9 @@ public function testDumpWithRouteNotFoundLocalizedRoutes() public function testDumpWithFallbackLocaleLocalizedRoutes() { - $this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_canonical_route', 'test')); - $this->routeCollection->add('test.nl', (new Route('/testen/is/leuk'))->setDefault('_canonical_route', 'test')); - $this->routeCollection->add('test.fr', (new Route('/tester/est/amusant'))->setDefault('_canonical_route', 'test')); + $this->routeCollection->add('test.en', (new Route('/testing/is/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'test')); + $this->routeCollection->add('test.nl', (new Route('/testen/is/leuk'))->setDefault('_locale', 'nl')->setDefault('_canonical_route', 'test')); + $this->routeCollection->add('test.fr', (new Route('/tester/est/amusant'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'test')); $code = $this->generatorDumper->dump(); file_put_contents($this->testTmpFilepath, $code); @@ -231,4 +231,29 @@ public function testDumpWithSchemeRequirement() $this->assertEquals('https://localhost/app.php/testing', $absoluteUrl); $this->assertEquals('/app.php/testing', $relativeUrl); } + + public function testDumpWithLocalizedRoutesPreserveTheGoodLocaleInTheUrl() + { + $this->routeCollection->add('foo.en', (new Route('/{_locale}/foo'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'foo')); + $this->routeCollection->add('foo.fr', (new Route('/{_locale}/foo'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'foo')); + $this->routeCollection->add('fun.en', (new Route('/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'fun')); + $this->routeCollection->add('fun.fr', (new Route('/amusant'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'fun')); + + file_put_contents($this->testTmpFilepath, $this->generatorDumper->dump()); + + $requestContext = new RequestContext(); + $requestContext->setParameter('_locale', 'fr'); + + $compiledUrlGenerator = new CompiledUrlGenerator(require $this->testTmpFilepath, $requestContext, null, null); + + $this->assertSame('/fr/foo', $compiledUrlGenerator->generate('foo')); + $this->assertSame('/en/foo', $compiledUrlGenerator->generate('foo.en')); + $this->assertSame('/en/foo', $compiledUrlGenerator->generate('foo', ['_locale' => 'en'])); + $this->assertSame('/en/foo', $compiledUrlGenerator->generate('foo.fr', ['_locale' => 'en'])); + + $this->assertSame('/amusant', $compiledUrlGenerator->generate('fun')); + $this->assertSame('/fun', $compiledUrlGenerator->generate('fun.en')); + $this->assertSame('/fun', $compiledUrlGenerator->generate('fun', ['_locale' => 'en'])); + $this->assertSame('/amusant', $compiledUrlGenerator->generate('fun.fr', ['_locale' => 'en'])); + } } diff --git a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php index a768384747684..01215da2c064e 100644 --- a/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php +++ b/src/Symfony/Component/Routing/Tests/Generator/UrlGeneratorTest.php @@ -236,6 +236,29 @@ public function testGenerateWithOverriddenParameterLocaleFromRequestContext() ); } + public function testDumpWithLocalizedRoutesPreserveTheGoodLocaleInTheUrl() + { + $routeCollection = new RouteCollection(); + + $routeCollection->add('foo.en', (new Route('/{_locale}/foo'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'foo')); + $routeCollection->add('foo.fr', (new Route('/{_locale}/foo'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'foo')); + $routeCollection->add('fun.en', (new Route('/fun'))->setDefault('_locale', 'en')->setDefault('_canonical_route', 'fun')); + $routeCollection->add('fun.fr', (new Route('/amusant'))->setDefault('_locale', 'fr')->setDefault('_canonical_route', 'fun')); + + $urlGenerator = $this->getGenerator($routeCollection); + $urlGenerator->getContext()->setParameter('_locale', 'fr'); + + $this->assertSame('/app.php/fr/foo', $urlGenerator->generate('foo')); + $this->assertSame('/app.php/en/foo', $urlGenerator->generate('foo.en')); + $this->assertSame('/app.php/en/foo', $urlGenerator->generate('foo', ['_locale' => 'en'])); + $this->assertSame('/app.php/en/foo', $urlGenerator->generate('foo.fr', ['_locale' => 'en'])); + + $this->assertSame('/app.php/amusant', $urlGenerator->generate('fun')); + $this->assertSame('/app.php/fun', $urlGenerator->generate('fun.en')); + $this->assertSame('/app.php/fun', $urlGenerator->generate('fun', ['_locale' => 'en'])); + $this->assertSame('/app.php/amusant', $urlGenerator->generate('fun.fr', ['_locale' => 'en'])); + } + public function testGenerateWithoutRoutes() { $this->expectException('Symfony\Component\Routing\Exception\RouteNotFoundException'); diff --git a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php index 4bc9596ce3f8c..cad50180d181e 100644 --- a/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php +++ b/src/Symfony/Component/Security/Core/Encoder/EncoderFactory.php @@ -144,10 +144,10 @@ private function getEncoderConfigFromAlgorithm(array $config): array return [ 'class' => Pbkdf2PasswordEncoder::class, 'arguments' => [ - $config['hash_algorithm'], - $config['encode_as_base64'], - $config['iterations'], - $config['key_length'], + $config['hash_algorithm'] ?? 'sha512', + $config['encode_as_base64'] ?? true, + $config['iterations'] ?? 1000, + $config['key_length'] ?? 40, ], ]; @@ -205,8 +205,8 @@ private function getEncoderConfigFromAlgorithm(array $config): array 'class' => MessageDigestPasswordEncoder::class, 'arguments' => [ $config['algorithm'], - $config['encode_as_base64'], - $config['iterations'], + $config['encode_as_base64'] ?? true, + $config['iterations'] ?? 5000, ], ]; } diff --git a/src/Symfony/Component/Security/Core/LICENSE b/src/Symfony/Component/Security/Core/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Security/Core/LICENSE +++ b/src/Symfony/Component/Security/Core/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Security/Core/README.md b/src/Symfony/Component/Security/Core/README.md index ede185bd3b3f5..70476d9e7f2b2 100644 --- a/src/Symfony/Component/Security/Core/README.md +++ b/src/Symfony/Component/Security/Core/README.md @@ -9,7 +9,7 @@ the Java Spring framework. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/security/index.html) + * [Documentation](https://symfony.com/doc/current/components/security.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php b/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php index a25f23528708e..44cecafecd2ba 100644 --- a/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Encoder/EncoderFactoryTest.php @@ -162,6 +162,11 @@ public function testDefaultMigratingEncoders() (new EncoderFactory([SomeUser::class => ['class' => NativePasswordEncoder::class, 'arguments' => []]]))->getEncoder(SomeUser::class) ); + $this->assertInstanceOf( + MigratingPasswordEncoder::class, + (new EncoderFactory([SomeUser::class => ['algorithm' => 'bcrypt', 'cost' => 11]]))->getEncoder(SomeUser::class) + ); + if (!SodiumPasswordEncoder::isSupported()) { return; } diff --git a/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php b/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php index aa9ade7020065..aaf3e5291c315 100644 --- a/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/User/ChainUserProviderTest.php @@ -70,24 +70,49 @@ public function testRefreshUser() $provider1 = $this->getProvider(); $provider1 ->expects($this->once()) - ->method('refreshUser') - ->willThrowException(new UnsupportedUserException('unsupported')) + ->method('supportsClass') + ->willReturn(false) ; $provider2 = $this->getProvider(); + $provider2 + ->expects($this->once()) + ->method('supportsClass') + ->willReturn(true) + ; + $provider2 + ->expects($this->once()) + ->method('refreshUser') + ->willThrowException(new UnsupportedUserException('unsupported')) + ; + + $provider3 = $this->getProvider(); + $provider3 + ->expects($this->once()) + ->method('supportsClass') + ->willReturn(true) + ; + + $provider3 ->expects($this->once()) ->method('refreshUser') ->willReturn($account = $this->getAccount()) ; - $provider = new ChainUserProvider([$provider1, $provider2]); + $provider = new ChainUserProvider([$provider1, $provider2, $provider3]); $this->assertSame($account, $provider->refreshUser($this->getAccount())); } public function testRefreshUserAgain() { $provider1 = $this->getProvider(); + $provider1 + ->expects($this->once()) + ->method('supportsClass') + ->willReturn(true) + ; + $provider1 ->expects($this->once()) ->method('refreshUser') @@ -95,6 +120,12 @@ public function testRefreshUserAgain() ; $provider2 = $this->getProvider(); + $provider2 + ->expects($this->once()) + ->method('supportsClass') + ->willReturn(true) + ; + $provider2 ->expects($this->once()) ->method('refreshUser') @@ -109,6 +140,12 @@ public function testRefreshUserThrowsUnsupportedUserException() { $this->expectException('Symfony\Component\Security\Core\Exception\UnsupportedUserException'); $provider1 = $this->getProvider(); + $provider1 + ->expects($this->once()) + ->method('supportsClass') + ->willReturn(true) + ; + $provider1 ->expects($this->once()) ->method('refreshUser') @@ -116,6 +153,12 @@ public function testRefreshUserThrowsUnsupportedUserException() ; $provider2 = $this->getProvider(); + $provider2 + ->expects($this->once()) + ->method('supportsClass') + ->willReturn(true) + ; + $provider2 ->expects($this->once()) ->method('refreshUser') @@ -173,6 +216,12 @@ public function testSupportsClassWhenNotSupported() public function testAcceptsTraversable() { $provider1 = $this->getProvider(); + $provider1 + ->expects($this->once()) + ->method('supportsClass') + ->willReturn(true) + ; + $provider1 ->expects($this->once()) ->method('refreshUser') @@ -180,6 +229,12 @@ public function testAcceptsTraversable() ; $provider2 = $this->getProvider(); + $provider2 + ->expects($this->once()) + ->method('supportsClass') + ->willReturn(true) + ; + $provider2 ->expects($this->once()) ->method('refreshUser') diff --git a/src/Symfony/Component/Security/Core/User/ChainUserProvider.php b/src/Symfony/Component/Security/Core/User/ChainUserProvider.php index 68bb91ec4790b..462a44e0cfb64 100644 --- a/src/Symfony/Component/Security/Core/User/ChainUserProvider.php +++ b/src/Symfony/Component/Security/Core/User/ChainUserProvider.php @@ -73,6 +73,10 @@ public function refreshUser(UserInterface $user) foreach ($this->providers as $provider) { try { + if (!$provider->supportsClass(\get_class($user))) { + continue; + } + return $provider->refreshUser($user); } catch (UnsupportedUserException $e) { // try next one diff --git a/src/Symfony/Component/Security/Csrf/LICENSE b/src/Symfony/Component/Security/Csrf/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Security/Csrf/LICENSE +++ b/src/Symfony/Component/Security/Csrf/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Security/Csrf/README.md b/src/Symfony/Component/Security/Csrf/README.md index aff72a0c32240..15b9ace238fb9 100644 --- a/src/Symfony/Component/Security/Csrf/README.md +++ b/src/Symfony/Component/Security/Csrf/README.md @@ -7,7 +7,7 @@ The Security CSRF (cross-site request forgery) component provides a class Resources --------- - * [Documentation](https://symfony.com/doc/current/components/security/index.html) + * [Documentation](https://symfony.com/doc/current/components/security.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php index b9c984f21d334..3846455e44884 100644 --- a/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php +++ b/src/Symfony/Component/Security/Guard/Firewall/GuardAuthenticationListener.php @@ -45,7 +45,7 @@ class GuardAuthenticationListener extends AbstractListener * @param string $providerKey The provider (i.e. firewall) key * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationProvider */ - public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, $guardAuthenticators, LoggerInterface $logger = null) + public function __construct(GuardAuthenticatorHandler $guardHandler, AuthenticationManagerInterface $authenticationManager, string $providerKey, iterable $guardAuthenticators, LoggerInterface $logger = null) { if (empty($providerKey)) { throw new \InvalidArgumentException('$providerKey must not be empty.'); diff --git a/src/Symfony/Component/Security/Guard/LICENSE b/src/Symfony/Component/Security/Guard/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Security/Guard/LICENSE +++ b/src/Symfony/Component/Security/Guard/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php b/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php index 4dd7a7b4466d2..dd2eeba33dea8 100644 --- a/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php +++ b/src/Symfony/Component/Security/Guard/PasswordAuthenticatedInterface.php @@ -19,7 +19,7 @@ interface PasswordAuthenticatedInterface /** * Returns the clear-text password contained in credentials if any. * - * @param mixed The user credentials + * @param mixed $credentials The user credentials */ public function getPassword($credentials): ?string; } diff --git a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php index 49eb9478721a1..99704b3e758e4 100644 --- a/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php @@ -48,7 +48,7 @@ class GuardAuthenticationProvider implements AuthenticationProviderInterface * @param iterable|AuthenticatorInterface[] $guardAuthenticators The authenticators, with keys that match what's passed to GuardAuthenticationListener * @param string $providerKey The provider (i.e. firewall) key */ - public function __construct($guardAuthenticators, UserProviderInterface $userProvider, string $providerKey, UserCheckerInterface $userChecker, UserPasswordEncoderInterface $passwordEncoder = null) + public function __construct(iterable $guardAuthenticators, UserProviderInterface $userProvider, string $providerKey, UserCheckerInterface $userChecker, UserPasswordEncoderInterface $passwordEncoder = null) { $this->guardAuthenticators = $guardAuthenticators; $this->userProvider = $userProvider; diff --git a/src/Symfony/Component/Security/Guard/README.md b/src/Symfony/Component/Security/Guard/README.md index ce706223901b3..40083a48d7682 100644 --- a/src/Symfony/Component/Security/Guard/README.md +++ b/src/Symfony/Component/Security/Guard/README.md @@ -8,7 +8,7 @@ total control. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/security/index.html) + * [Documentation](https://symfony.com/doc/current/components/security.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index 69066552d7ca6..d8a157e80b142 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -201,12 +201,17 @@ protected function refreshUser(TokenInterface $token): ?TokenInterface $userNotFoundByProvider = false; $userDeauthenticated = false; + $userClass = \get_class($user); foreach ($this->userProviders as $provider) { if (!$provider instanceof UserProviderInterface) { throw new \InvalidArgumentException(sprintf('User provider "%s" must implement "%s".', \get_class($provider), UserProviderInterface::class)); } + if (!$provider->supportsClass($userClass)) { + continue; + } + try { $refreshedUser = $provider->refreshUser($user); $newToken = clone $token; @@ -263,7 +268,7 @@ protected function refreshUser(TokenInterface $token): ?TokenInterface return null; } - throw new \RuntimeException(sprintf('There is no user provider for user "%s".', \get_class($user))); + throw new \RuntimeException(sprintf('There is no user provider for user "%s".', $userClass)); } private function safelyUnserialize(string $serializedToken) diff --git a/src/Symfony/Component/Security/Http/LICENSE b/src/Symfony/Component/Security/Http/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Security/Http/LICENSE +++ b/src/Symfony/Component/Security/Http/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Security/Http/README.md b/src/Symfony/Component/Security/Http/README.md index 5be2111830116..dbac8c659c112 100644 --- a/src/Symfony/Component/Security/Http/README.md +++ b/src/Symfony/Component/Security/Http/README.md @@ -9,7 +9,7 @@ the Java Spring framework. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/security/index.html) + * [Documentation](https://symfony.com/doc/current/components/security.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php index 487f864e9b071..55fd885918ba6 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/AbstractRememberMeServices.php @@ -94,6 +94,10 @@ public function getSecret() */ final public function autoLogin(Request $request): ?TokenInterface { + if (($cookie = $request->attributes->get(self::COOKIE_ATTR_NAME)) && null === $cookie->getValue()) { + return null; + } + if (null === $cookie = $request->cookies->get($this->options['name'])) { return null; } diff --git a/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php b/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php index f11e1dc02f041..33427517ca4ee 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php +++ b/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php @@ -91,12 +91,12 @@ protected function onLoginSuccess(Request $request, Response $response, TokenInt /** * Generates the cookie value. * - * @param int $expires The Unix timestamp when the cookie expires - * @param string $password The encoded password + * @param int $expires The Unix timestamp when the cookie expires + * @param string|null $password The encoded password * * @return string */ - protected function generateCookieValue(string $class, string $username, int $expires, string $password) + protected function generateCookieValue(string $class, string $username, int $expires, ?string $password) { // $username is encoded because it might contain COOKIE_DELIMITER, // we assume other values don't @@ -111,12 +111,12 @@ protected function generateCookieValue(string $class, string $username, int $exp /** * Generates a hash for the cookie to ensure it is not being tampered with. * - * @param int $expires The Unix timestamp when the cookie expires - * @param string $password The encoded password + * @param int $expires The Unix timestamp when the cookie expires + * @param string|null $password The encoded password * * @return string */ - protected function generateCookieHash(string $class, string $username, int $expires, string $password) + protected function generateCookieHash(string $class, string $username, int $expires, ?string $password) { return hash_hmac('sha256', $class.self::COOKIE_DELIMITER.$username.self::COOKIE_DELIMITER.$expires.self::COOKIE_DELIMITER.$password, $this->getSecret()); } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php index a6f118f6490a2..13ec17289ab5d 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php @@ -253,7 +253,7 @@ public function testHandleRemovesTokenIfNoPreviousSessionWasFound() public function testIfTokenIsDeauthenticated() { $refreshedUser = new User('foobar', 'baz'); - $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)]); + $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)]); $this->assertNull($tokenStorage->getToken()); } @@ -275,7 +275,7 @@ public function testRememberMeGetsCanceledIfTokenIsDeauthenticated() $rememberMeServices = $this->createMock(RememberMeServicesInterface::class); $rememberMeServices->expects($this->once())->method('loginFail'); - $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)], null, $rememberMeServices); + $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)], null, $rememberMeServices); $this->assertNull($tokenStorage->getToken()); } @@ -283,7 +283,7 @@ public function testRememberMeGetsCanceledIfTokenIsDeauthenticated() public function testTryAllUserProvidersUntilASupportingUserProviderIsFound() { $refreshedUser = new User('foobar', 'baz'); - $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)], $refreshedUser); + $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)], $refreshedUser); $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser()); } @@ -291,14 +291,14 @@ public function testTryAllUserProvidersUntilASupportingUserProviderIsFound() public function testNextSupportingUserProviderIsTriedIfPreviousSupportingUserProviderDidNotLoadTheUser() { $refreshedUser = new User('foobar', 'baz'); - $tokenStorage = $this->handleEventWithPreviousSession([new SupportingUserProvider(), new SupportingUserProvider($refreshedUser)], $refreshedUser); + $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)], $refreshedUser); $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser()); } public function testTokenIsSetToNullIfNoUserWasLoadedByTheRegisteredUserProviders() { - $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new SupportingUserProvider()]); + $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider()]); $this->assertNull($tokenStorage->getToken()); } @@ -306,13 +306,13 @@ public function testTokenIsSetToNullIfNoUserWasLoadedByTheRegisteredUserProvider public function testRuntimeExceptionIsThrownIfNoSupportingUserProviderWasRegistered() { $this->expectException('RuntimeException'); - $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new NotSupportingUserProvider()]); + $this->handleEventWithPreviousSession([new NotSupportingUserProvider(false), new NotSupportingUserProvider(true)]); } public function testAcceptsProvidersAsTraversable() { $refreshedUser = new User('foobar', 'baz'); - $tokenStorage = $this->handleEventWithPreviousSession(new \ArrayObject([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)]), $refreshedUser); + $tokenStorage = $this->handleEventWithPreviousSession(new \ArrayObject([new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)]), $refreshedUser); $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser()); } @@ -338,7 +338,7 @@ public function testDeauthenticatedEvent() $this->assertNotEquals($event->getRefreshedToken()->getUser(), $user); }); - $listener = new ContextListener($tokenStorage, [new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)], 'context_key', null, $eventDispatcher); + $listener = new ContextListener($tokenStorage, [new NotSupportingUserProvider(true), new NotSupportingUserProvider(false), new SupportingUserProvider($refreshedUser)], 'context_key', null, $eventDispatcher); $listener(new RequestEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST)); $this->assertNull($tokenStorage->getToken()); @@ -422,6 +422,14 @@ private function handleEventWithPreviousSession($userProviders, UserInterface $u class NotSupportingUserProvider implements UserProviderInterface { + /** @var bool */ + private $throwsUnsupportedException; + + public function __construct($throwsUnsupportedException) + { + $this->throwsUnsupportedException = $throwsUnsupportedException; + } + public function loadUserByUsername($username): UserInterface { throw new UsernameNotFoundException(); @@ -429,7 +437,11 @@ public function loadUserByUsername($username): UserInterface public function refreshUser(UserInterface $user): UserInterface { - throw new UnsupportedUserException(); + if ($this->throwsUnsupportedException) { + throw new UnsupportedUserException(); + } + + return $user; } public function supportsClass($class): bool diff --git a/src/Symfony/Component/Security/Http/Tests/RememberMe/AbstractRememberMeServicesTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/AbstractRememberMeServicesTest.php index c476e65403c2e..7a0506dfe840b 100644 --- a/src/Symfony/Component/Security/Http/Tests/RememberMe/AbstractRememberMeServicesTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/AbstractRememberMeServicesTest.php @@ -39,6 +39,17 @@ public function testAutoLoginReturnsNullWhenNoCookie() $this->assertNull($service->autoLogin(new Request())); } + public function testAutoLoginReturnsNullAfterLoginFail() + { + $service = $this->getService(null, ['name' => 'foo', 'path' => null, 'domain' => null]); + + $request = new Request(); + $request->cookies->set('foo', 'foo'); + + $service->loginFail($request); + $this->assertNull($service->autoLogin($request)); + } + public function testAutoLoginThrowsExceptionWhenImplementationDoesNotReturnUserInterface() { $this->expectException('RuntimeException'); diff --git a/src/Symfony/Component/Serializer/LICENSE b/src/Symfony/Component/Serializer/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Serializer/LICENSE +++ b/src/Symfony/Component/Serializer/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php index 80840a042f2fc..7ce17cc399374 100644 --- a/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php +++ b/src/Symfony/Component/Serializer/NameConverter/MetadataAwareNameConverter.php @@ -47,7 +47,7 @@ public function normalize(string $propertyName, string $class = null, string $fo return $this->normalizeFallback($propertyName, $class, $format, $context); } - if (!isset(self::$normalizeCache[$class][$propertyName])) { + if (!\array_key_exists($class, self::$normalizeCache) || !\array_key_exists($propertyName, self::$normalizeCache[$class])) { self::$normalizeCache[$class][$propertyName] = $this->getCacheValueForNormalization($propertyName, $class); } @@ -64,7 +64,7 @@ public function denormalize(string $propertyName, string $class = null, string $ } $cacheKey = $this->getCacheKey($class, $context); - if (!isset(self::$denormalizeCache[$cacheKey][$propertyName])) { + if (!\array_key_exists($cacheKey, self::$denormalizeCache) || !\array_key_exists($propertyName, self::$denormalizeCache[$cacheKey])) { self::$denormalizeCache[$cacheKey][$propertyName] = $this->getCacheValueForDenormalization($propertyName, $class, $context); } @@ -78,7 +78,7 @@ private function getCacheValueForNormalization(string $propertyName, string $cla } $attributesMetadata = $this->metadataFactory->getMetadataFor($class)->getAttributesMetadata(); - if (!isset($attributesMetadata[$propertyName])) { + if (!\array_key_exists($propertyName, $attributesMetadata)) { return null; } @@ -93,7 +93,7 @@ private function normalizeFallback(string $propertyName, string $class = null, s private function getCacheValueForDenormalization(string $propertyName, string $class, array $context): ?string { $cacheKey = $this->getCacheKey($class, $context); - if (!isset(self::$attributesMetadataCache[$cacheKey])) { + if (!\array_key_exists($cacheKey, self::$attributesMetadataCache)) { self::$attributesMetadataCache[$cacheKey] = $this->getCacheValueForAttributesMetadata($class, $context); } diff --git a/src/Symfony/Component/Stopwatch/LICENSE b/src/Symfony/Component/Stopwatch/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Stopwatch/LICENSE +++ b/src/Symfony/Component/Stopwatch/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/String/AbstractUnicodeString.php b/src/Symfony/Component/String/AbstractUnicodeString.php index 19ad5e8939d95..bc3e05cd6f10c 100644 --- a/src/Symfony/Component/String/AbstractUnicodeString.php +++ b/src/Symfony/Component/String/AbstractUnicodeString.php @@ -172,7 +172,17 @@ public function codePointsAt(int $offset): array { $str = $this->slice($offset, 1); - return '' === $str->string ? [] : array_map('mb_ord', preg_split('//u', $str->string, -1, PREG_SPLIT_NO_EMPTY)); + if ('' === $str->string) { + return []; + } + + $codePoints = []; + + foreach (preg_split('//u', $str->string, -1, PREG_SPLIT_NO_EMPTY) as $c) { + $codePoints[] = mb_ord($c, 'UTF-8'); + } + + return $codePoints; } public function folded(bool $compat = true): parent diff --git a/src/Symfony/Component/String/CodePointString.php b/src/Symfony/Component/String/CodePointString.php index 8a729bb9e1623..d1ac91570181d 100644 --- a/src/Symfony/Component/String/CodePointString.php +++ b/src/Symfony/Component/String/CodePointString.php @@ -79,7 +79,7 @@ public function codePointsAt(int $offset): array { $str = $offset ? $this->slice($offset, 1) : $this; - return '' === $str->string ? [] : [mb_ord($str->string)]; + return '' === $str->string ? [] : [mb_ord($str->string, 'UTF-8')]; } public function endsWith($suffix): bool diff --git a/src/Symfony/Component/String/LICENSE b/src/Symfony/Component/String/LICENSE index 1a1869751d250..4bf0fef4ff3b0 100644 --- a/src/Symfony/Component/String/LICENSE +++ b/src/Symfony/Component/String/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Fabien Potencier +Copyright (c) 2019-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php index 934b29c1486bd..0508a36b3f428 100644 --- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php @@ -55,6 +55,50 @@ public static function provideBytesAt(): array ]; } + /** + * @dataProvider wordwrapProvider + */ + public function testWordwrap($expected, $actual, $length, $break, $cut = false) + { + $instance = static::createFromString($actual); + $actual = $instance->wordwrap($length, $break, $cut); + + $this->assertEquals($expected, $actual); + } + + public function wordwrapProvider() + { + return [ + [ + 'Lo-re-m-Ip-su-m', + 'Lorem Ipsum', + 2, + '-', + true, + ], + [ + 'Lorem-Ipsum', + 'Lorem Ipsum', + 2, + '-', + ], + [ + 'Lor-em-Ips-um', + 'Lorem Ipsum', + 3, + '-', + true, + ], + [ + 'L-o-r-e-m-I-p-s-u-m', + 'Lorem Ipsum', + 1, + '-', + true, + ], + ]; + } + /** * @dataProvider provideWrap */ diff --git a/src/Symfony/Component/String/UnicodeString.php b/src/Symfony/Component/String/UnicodeString.php index d7df6f5e1400b..ada77caaae4c0 100644 --- a/src/Symfony/Component/String/UnicodeString.php +++ b/src/Symfony/Component/String/UnicodeString.php @@ -71,7 +71,7 @@ public function chunk(int $length = 1): array $rx .= '\X{65535}'; $length -= 65535; } - $rx .= '\X{'.$length.'})/us'; + $rx .= '\X{'.$length.'})/u'; $str = clone $this; $chunks = []; diff --git a/src/Symfony/Component/Templating/LICENSE b/src/Symfony/Component/Templating/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Templating/LICENSE +++ b/src/Symfony/Component/Templating/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Templating/README.md b/src/Symfony/Component/Templating/README.md index 58e2a0a5c6b83..2b8ecad873964 100644 --- a/src/Symfony/Component/Templating/README.md +++ b/src/Symfony/Component/Templating/README.md @@ -12,7 +12,7 @@ layouts. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/templating/index.html) + * [Documentation](https://symfony.com/doc/current/components/templating.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php b/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php index 4c4f31aea87e8..3b0042946f01f 100644 --- a/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php +++ b/src/Symfony/Component/Translation/DataCollector/TranslationDataCollector.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; use Symfony\Component\Translation\DataCollectorTranslator; +use Symfony\Component\VarDumper\Cloner\Data; /** * @author Abdellatif Ait boudad diff --git a/src/Symfony/Component/Translation/Dumper/FileDumper.php b/src/Symfony/Component/Translation/Dumper/FileDumper.php index 463a08c75b5c6..e257e722461d0 100644 --- a/src/Symfony/Component/Translation/Dumper/FileDumper.php +++ b/src/Symfony/Component/Translation/Dumper/FileDumper.php @@ -51,37 +51,36 @@ public function dump(MessageCatalogue $messages, array $options = []) throw new InvalidArgumentException('The file dumper needs a path option.'); } - $hasMessageFormatter = class_exists(\MessageFormatter::class); - // save a file for each domain foreach ($messages->getDomains() as $domain) { - if ($hasMessageFormatter) { - $defaultDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX; - $altDomain = $domain; - } else { - $defaultDomain = $domain; - $altDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX; - } - $defaultPath = $options['path'].'/'.$this->getRelativePath($defaultDomain, $messages->getLocale()); - $altPath = $options['path'].'/'.$this->getRelativePath($altDomain, $messages->getLocale()); - - if (!file_exists($defaultPath) && file_exists($altPath)) { - [$defaultPath, $altPath] = [$altPath, $defaultPath]; - } - - if (!file_exists($defaultPath)) { - $directory = \dirname($defaultPath); + $fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale()); + if (!file_exists($fullpath)) { + $directory = \dirname($fullpath); if (!file_exists($directory) && !@mkdir($directory, 0777, true)) { throw new RuntimeException(sprintf('Unable to create directory "%s".', $directory)); } } - if (file_exists($altPath)) { - // clear alternative translation file - file_put_contents($altPath, $this->formatCatalogue(new MessageCatalogue($messages->getLocale()), $altDomain, $options)); + $intlDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX; + $intlMessages = $messages->all($intlDomain); + + if ($intlMessages) { + $intlPath = $options['path'].'/'.$this->getRelativePath($intlDomain, $messages->getLocale()); + file_put_contents($intlPath, $this->formatCatalogue($messages, $intlDomain, $options)); + + $messages->replace([], $intlDomain); + + try { + if ($messages->all($domain)) { + file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); + } + continue; + } finally { + $messages->replace($intlMessages, $intlDomain); + } } - file_put_contents($defaultPath, $this->formatCatalogue($messages, $domain, $options)); + file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); } } diff --git a/src/Symfony/Component/Translation/LICENSE b/src/Symfony/Component/Translation/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Translation/LICENSE +++ b/src/Symfony/Component/Translation/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Translation/MessageCatalogue.php b/src/Symfony/Component/Translation/MessageCatalogue.php index e0b18e9801038..429a3c687747b 100644 --- a/src/Symfony/Component/Translation/MessageCatalogue.php +++ b/src/Symfony/Component/Translation/MessageCatalogue.php @@ -156,7 +156,9 @@ public function add(array $messages, string $domain = 'messages') if (!isset($this->messages[$domain])) { $this->messages[$domain] = $messages; } else { - $this->messages[$domain] = array_replace($this->messages[$domain], $messages); + foreach ($messages as $id => $message) { + $this->messages[$domain][$id] = $message; + } } } diff --git a/src/Symfony/Component/Translation/README.md b/src/Symfony/Component/Translation/README.md index 46f3d1f2f25e6..e80a70cad033e 100644 --- a/src/Symfony/Component/Translation/README.md +++ b/src/Symfony/Component/Translation/README.md @@ -6,7 +6,7 @@ The Translation component provides tools to internationalize your application. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/translation/index.html) + * [Documentation](https://symfony.com/doc/current/components/translation.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Translation/Tests/Dumper/FileDumperTest.php b/src/Symfony/Component/Translation/Tests/Dumper/FileDumperTest.php index 9dd1377e49729..6e42b1e5683e5 100644 --- a/src/Symfony/Component/Translation/Tests/Dumper/FileDumperTest.php +++ b/src/Symfony/Component/Translation/Tests/Dumper/FileDumperTest.php @@ -27,15 +27,11 @@ public function testDump() $dumper = new ConcreteFileDumper(); $dumper->dump($catalogue, ['path' => $tempDir]); - $suffix = class_exists(\MessageFormatter::class) ? '+intl-icu' : ''; - $this->assertFileExists($tempDir."/messages$suffix.en.concrete"); + $this->assertFileExists($tempDir.'/messages.en.concrete'); - @unlink($tempDir."/messages$suffix.en.concrete"); + @unlink($tempDir.'/messages.en.concrete'); } - /** - * @requires extension intl - */ public function testDumpIntl() { $tempDir = sys_get_temp_dir(); @@ -46,11 +42,13 @@ public function testDumpIntl() $catalogue->add(['bar' => 'foo'], 'd2+intl-icu'); $dumper = new ConcreteFileDumper(); + @unlink($tempDir.'/d2.en.concrete'); $dumper->dump($catalogue, ['path' => $tempDir]); - $this->assertFileNotExists($tempDir.'/d1.en.concrete'); + $this->assertStringEqualsFile($tempDir.'/d1.en.concrete', 'foo=bar'); + @unlink($tempDir.'/d1.en.concrete'); - $this->assertStringEqualsFile($tempDir.'/d1+intl-icu.en.concrete', 'bar=foo&foo=bar'); + $this->assertStringEqualsFile($tempDir.'/d1+intl-icu.en.concrete', 'bar=foo'); @unlink($tempDir.'/d1+intl-icu.en.concrete'); $this->assertFileNotExists($tempDir.'/d2.en.concrete'); @@ -62,8 +60,7 @@ public function testDumpCreatesNestedDirectoriesAndFile() { $tempDir = sys_get_temp_dir(); $translationsDir = $tempDir.'/test/translations'; - $suffix = class_exists(\MessageFormatter::class) ? '+intl-icu' : ''; - $file = $translationsDir."/messages$suffix.en.concrete"; + $file = $translationsDir.'/messages.en.concrete'; $catalogue = new MessageCatalogue('en'); $catalogue->add(['foo' => 'bar']); diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index 9666ac97ea0da..81a438fa577ef 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -263,15 +263,38 @@ public function testTransWithIcuRootFallbackLocale() $this->assertSame('bar', $translator->trans('bar')); } - public function testTransWithFallbackLocaleBis() + /** + * @dataProvider getFallbackLocales + */ + public function testTransWithFallbackLocaleBis($expectedLocale, $locale) { - $translator = new Translator('en_US'); + $translator = new Translator($locale); $translator->addLoader('array', new ArrayLoader()); - $translator->addResource('array', ['foo' => 'foofoo'], 'en_US'); - $translator->addResource('array', ['bar' => 'foobar'], 'en'); + $translator->addResource('array', ['foo' => 'foofoo'], $locale); + $translator->addResource('array', ['bar' => 'foobar'], $expectedLocale); $this->assertEquals('foobar', $translator->trans('bar')); } + public function getFallbackLocales() + { + $locales = [ + ['en', 'en_US'], + ['en', 'en-US'], + ['sl_Latn_IT', 'sl_Latn_IT_nedis'], + ['sl_Latn', 'sl_Latn_IT'], + ]; + + if (\function_exists('locale_parse')) { + $locales[] = ['sl_Latn_IT', 'sl-Latn-IT-nedis']; + $locales[] = ['sl_Latn', 'sl-Latn-IT']; + } else { + $locales[] = ['sl-Latn-IT', 'sl-Latn-IT-nedis']; + $locales[] = ['sl-Latn', 'sl-Latn-IT']; + } + + return $locales; + } + public function testTransWithFallbackLocaleTer() { $translator = new Translator('fr_FR'); diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index b14a7ed5590a8..24910f0827c04 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -405,10 +405,17 @@ protected function computeFallbackLocales(string $locale) while ($locale) { $parent = $parentLocales[$locale] ?? null; - if (!$parent && false !== strrchr($locale, '_')) { - $locale = substr($locale, 0, -\strlen(strrchr($locale, '_'))); - } elseif ('root' !== $parent) { - $locale = $parent; + if ($parent) { + $locale = 'root' !== $parent ? $parent : null; + } elseif (\function_exists('locale_parse')) { + $localeSubTags = locale_parse($locale); + $locale = null; + if (1 < \count($localeSubTags)) { + array_pop($localeSubTags); + $locale = locale_compose($localeSubTags) ?: null; + } + } elseif ($i = strrpos($locale, '_') ?: strrpos($locale, '-')) { + $locale = substr($locale, 0, $i); } else { $locale = null; } diff --git a/src/Symfony/Component/Validator/LICENSE b/src/Symfony/Component/Validator/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Validator/LICENSE +++ b/src/Symfony/Component/Validator/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf index 12f26fdc88688..79dab63d7de58 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf @@ -334,6 +334,38 @@ This value should be valid JSON. هذه القيمة يجب أن تكون صالحة ل JSON. + + This collection should contain only unique elements. + يجب أن تحتوي هذه المجموعة علي عناصر فريدة فقط. + + + This value should be positive. + يجب أن تكون هذه القيمة موجبة. + + + This value should be either positive or zero. + يجب أن تكون هذه القيمة إما موجبة او صفر. + + + This value should be negative. + يجب أن تكون هذه القيمة سالبة. + + + This value should be either negative or zero. + يجب أن تكون هذه القيمة إما سالبة او صفر. + + + This value is not a valid timezone. + هذه القيمة ليست منطقة زمنية صحيحة. + + + 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.sr_Latn.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.sr_Latn.xlf index 018dd1233ac61..20dff43c6d904 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.sr_Latn.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.sr_Latn.xlf @@ -24,11 +24,11 @@ You must select at least {{ limit }} choice.|You must select at least {{ limit }} choices. - Morate odabrati bar {{ limit }} mogućnost.|Morate odabrati bar {{ limit }} mogućnosti. + Morate odabrati bar {{ limit }} mogućnost.|Morate odabrati bar {{ limit }} mogućnosti.|Morate odabrati bar {{ limit }} mogućnosti. You must select at most {{ limit }} choice.|You must select at most {{ limit }} choices. - Morate odabrati najviše {{ limit }} mogućnost.|Morate odabrati najviše {{ limit }} mogućnosti. + Morate odabrati najviše {{ limit }} mogućnost.|Morate odabrati najviše {{ limit }} mogućnosti.|Morate odabrati najviše {{ limit }} mogućnosti. One or more of the given values is invalid. @@ -76,7 +76,7 @@ This value is too long. It should have {{ limit }} character or less.|This value is too long. It should have {{ limit }} characters or less. - Vrednost je predugačka. Trebalo bi da ima {{ limit }} karakter ili manje.|Vrednost je predugačka. Trebalo bi da ima {{ limit }} karaktera ili manje. + Vrednost je predugačka. Trebalo bi da ima {{ limit }} karakter ili manje.|Vrednost je predugačka. Trebalo bi da ima {{ limit }} karaktera ili manje.|Vrednost je predugačka. Trebalo bi da ima {{ limit }} karaktera ili manje. This value should be {{ limit }} or more. @@ -84,7 +84,7 @@ This value is too short. It should have {{ limit }} character or more.|This value is too short. It should have {{ limit }} characters or more. - Vrednost je prekratka. Trebalo bi da ima {{ limit }} karakter ili više.|Vrednost je prekratka. Trebalo bi da ima {{ limit }} karaktera ili više. + Vrednost je prekratka. Trebalo bi da ima {{ limit }} karakter ili više.|Vrednost je prekratka. Trebalo bi da ima {{ limit }} karaktera ili više.|Vrednost je prekratka. Trebalo bi da ima {{ limit }} karaktera ili više. This value should not be blank. @@ -180,7 +180,7 @@ This value should have exactly {{ limit }} character.|This value should have exactly {{ limit }} characters. - Vrednost bi trebalo da ima tačno {{ limit }} karakter.|Vrednost bi trebalo da ima tačno {{ limit }} karaktera. + Vrednost bi trebalo da ima tačno {{ limit }} karakter.|Vrednost bi trebalo da ima tačno {{ limit }} karaktera.|Vrednost bi trebalo da ima tačno {{ limit }} karaktera. The file was only partially uploaded. @@ -204,15 +204,15 @@ This collection should contain {{ limit }} element or more.|This collection should contain {{ limit }} elements or more. - Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata. + Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili više elemenata. This collection should contain {{ limit }} element or less.|This collection should contain {{ limit }} elements or less. - Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata. + Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata.|Ova kolekcija bi trebalo da sadrži {{ limit }} ili manje elemenata. This collection should contain exactly {{ limit }} element.|This collection should contain exactly {{ limit }} elements. - Ova kolekcija bi trebalo da sadrži tačno {{ limit }} element.|Ova kolekcija bi trebalo da sadrži tačno {{ limit }} elemenata. + Ova kolekcija bi trebalo da sadrži tačno {{ limit }} element.|Ova kolekcija bi trebalo da sadrži tačno {{ limit }} elementa.|Ova kolekcija bi trebalo da sadrži tačno {{ limit }} elemenata. Invalid card number. diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.zh_TW.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.zh_TW.xlf index d9d5f2f622b43..7cef875f5812e 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.zh_TW.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.zh_TW.xlf @@ -278,6 +278,94 @@ This value should not be identical to {{ compared_value_type }} {{ compared_value }}. 該值不應與 {{ compared_value_type }} {{ compared_value }} 相同。 + + The image ratio is too big ({{ ratio }}). Allowed maximum ratio is {{ max_ratio }}. + 圖像格式過大 ({{ ratio }})。 最大允許尺寸 {{ max_ratio }}。 + + + The image ratio is too small ({{ ratio }}). Minimum ratio expected is {{ min_ratio }}. + 圖像格式過小 ({{ ratio }})。最小尺寸 {{ min_ratio }}。 + + + The image is square ({{ width }}x{{ height }}px). Square images are not allowed. + 方形圖像 ({{ width }}x{{ height }}px)。不接受方形圖像。 + + + The image is landscape oriented ({{ width }}x{{ height }}px). Landscape oriented images are not allowed. + 紀念冊布局圖像 ({{ width }}x{{ height }}px)。 不接受紀念冊布局圖像。 + + + The image is portrait oriented ({{ width }}x{{ height }}px). Portrait oriented images are not allowed. + 書籍布局圖像 ({{ width }}x{{ height }}px)。不接受圖像書籍布局。 + + + An empty file is not allowed. + 不接受空白文件。 + + + The host could not be resolved. + 未找到服務器。 + + + This value does not match the expected {{ charset }} charset. + 該數值不符合預期 {{ charset }} 符號編碼。 + + + This is not a valid Business Identifier Code (BIC). + 無效企業識別碼 (BIC)。 + + + Error. + 錯誤。 + + + This is not a valid UUID. + 無效的通用唯壹標識符 (UUID)。 + + + This value should be a multiple of {{ compared_value }}. + 該值必須是倍數 {{ compared_value }}。 + + + This Business Identifier Code (BIC) is not associated with IBAN {{ iban }}. + 該企業識別碼 (BIC) 與銀行賬戶國際編號不壹致 (IBAN) {{ iban }}。 + + + This value should be valid JSON. + 該數值必須序列化為JSON格式。 + + + This collection should contain only unique elements. + 該集合應僅包含唯壹元素。 + + + This value should be positive. + 數值應為正數。 + + + This value should be either positive or zero. + 數值應或未正數,或為零。 + + + This value should be negative. + 數值應為負數。 + + + This value should be either negative or zero. + 數值應或未負數,或為零。 + + + This value is not a valid timezone. + 無效時區。 + + + 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/VarDumper/Dumper/ContextProvider/CliContextProvider.php b/src/Symfony/Component/VarDumper/Dumper/ContextProvider/CliContextProvider.php index e7f8ccf17f194..38f878971c53f 100644 --- a/src/Symfony/Component/VarDumper/Dumper/ContextProvider/CliContextProvider.php +++ b/src/Symfony/Component/VarDumper/Dumper/ContextProvider/CliContextProvider.php @@ -25,7 +25,7 @@ public function getContext(): ?array } return [ - 'command_line' => $commandLine = implode(' ', $_SERVER['argv']), + 'command_line' => $commandLine = implode(' ', $_SERVER['argv'] ?? []), 'identifier' => hash('crc32b', $commandLine.$_SERVER['REQUEST_TIME_FLOAT']), ]; } diff --git a/src/Symfony/Component/VarDumper/LICENSE b/src/Symfony/Component/VarDumper/LICENSE index cf8b3ebe87145..684fbf94df83c 100644 --- a/src/Symfony/Component/VarDumper/LICENSE +++ b/src/Symfony/Component/VarDumper/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2019 Fabien Potencier +Copyright (c) 2014-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/VarExporter/LICENSE b/src/Symfony/Component/VarExporter/LICENSE index 3f853aaf35fe1..69d925ba7511e 100644 --- a/src/Symfony/Component/VarExporter/LICENSE +++ b/src/Symfony/Component/VarExporter/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 Fabien Potencier +Copyright (c) 2018-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/WebLink/LICENSE b/src/Symfony/Component/WebLink/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/WebLink/LICENSE +++ b/src/Symfony/Component/WebLink/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Workflow/LICENSE b/src/Symfony/Component/Workflow/LICENSE index cf8b3ebe87145..684fbf94df83c 100644 --- a/src/Symfony/Component/Workflow/LICENSE +++ b/src/Symfony/Component/Workflow/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2019 Fabien Potencier +Copyright (c) 2014-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index fc662b5d43ca3..e5e0f2a4bd5b1 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -439,6 +439,11 @@ private static function parseMapping(string $mapping, int $flags, int &$i = 0, a throw new ParseException('Missing mapping key.', self::$parsedLineNumber + 1, $mapping); } + if ('!php/const' === $key) { + $key .= ' '.self::parseScalar($mapping, $flags, [':'], $i, false, []); + $key = self::evaluateScalar($key, $flags); + } + if (false === $i = strpos($mapping, ':', $i)) { break; } @@ -674,6 +679,10 @@ private static function parseTag(string $value, int &$i, int $flags): ?string $nextOffset = $i + $tagLength + 1; $nextOffset += strspn($value, ' ', $nextOffset); + if ('' === $tag && (!isset($value[$nextOffset]) || \in_array($value[$nextOffset], [']', '}', ','], true))) { + throw new ParseException(sprintf('Using the unquoted scalar value "!" is not supported. You must quote it.', $value), self::$parsedLineNumber + 1, $value, self::$parsedFilename); + } + // Is followed by a scalar and is a built-in tag if ('' !== $tag && (!isset($value[$nextOffset]) || !\in_array($value[$nextOffset], ['[', '{'], true)) && ('!' === $tag[0] || 'str' === $tag || 'php/const' === $tag || 'php/object' === $tag)) { // Manage in {@link self::evaluateScalar()} diff --git a/src/Symfony/Component/Yaml/LICENSE b/src/Symfony/Component/Yaml/LICENSE index a677f43763ca4..9e936ec0448b8 100644 --- a/src/Symfony/Component/Yaml/LICENSE +++ b/src/Symfony/Component/Yaml/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Yaml/README.md b/src/Symfony/Component/Yaml/README.md index 0d324881ce5e4..b914e7836c7a5 100644 --- a/src/Symfony/Component/Yaml/README.md +++ b/src/Symfony/Component/Yaml/README.md @@ -6,7 +6,7 @@ The Yaml component loads and dumps YAML files. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/yaml/index.html) + * [Documentation](https://symfony.com/doc/current/components/yaml.html) * [Contributing](https://symfony.com/doc/current/contributing/index.html) * [Report issues](https://github.com/symfony/symfony/issues) and [send Pull Requests](https://github.com/symfony/symfony/pulls) diff --git a/src/Symfony/Component/Yaml/Tests/InlineTest.php b/src/Symfony/Component/Yaml/Tests/InlineTest.php index 9b07f8725d53e..0f46f75ee72a7 100644 --- a/src/Symfony/Component/Yaml/Tests/InlineTest.php +++ b/src/Symfony/Component/Yaml/Tests/InlineTest.php @@ -59,6 +59,7 @@ public function getTestsForParsePhpConstants() ['!php/const PHP_INT_MAX', PHP_INT_MAX], ['[!php/const PHP_INT_MAX]', [PHP_INT_MAX]], ['{ foo: !php/const PHP_INT_MAX }', ['foo' => PHP_INT_MAX]], + ['{ !php/const PHP_INT_MAX: foo }', [PHP_INT_MAX => 'foo']], ['!php/const NULL', null], ]; } @@ -736,4 +737,69 @@ public function getTestsForOctalNumbers() 'negative octal number' => [-28, '-034'], ]; } + + /** + * @dataProvider unquotedExclamationMarkThrowsProvider + */ + public function testUnquotedExclamationMarkThrows(string $value) + { + $this->expectException(ParseException::class); + $this->expectExceptionMessageRegExp('/^Using the unquoted scalar value "!" is not supported\. You must quote it at line 1 \(near "/'); + + Inline::parse($value); + } + + public function unquotedExclamationMarkThrowsProvider() + { + return [ + ['!'], + ['! '], + ['! '], + [' ! '], + ['[!]'], + ['[! ]'], + ['[! ]'], + ['[!, "foo"]'], + ['["foo", !, "ccc"]'], + ['{foo: !}'], + ['{foo: !}'], + ['{foo: !, bar: "ccc"}'], + ['{bar: "ccc", foo: ! }'], + ['!]]]'], + ['!}'], + ['!,}foo,]'], + ['! [!]'], + ]; + } + + /** + * @dataProvider quotedExclamationMarkProvider + */ + public function testQuotedExclamationMark($expected, string $value) + { + $this->assertSame($expected, Inline::parse($value)); + } + + // This provider should stay consistent with unquotedExclamationMarkThrowsProvider + public function quotedExclamationMarkProvider() + { + return [ + ['!', '"!"'], + ['! ', '"! "'], + [' !', '" !"'], + [' ! ', '" ! "'], + [['!'], '["!"]'], + [['! '], '["! "]'], + [['!', 'foo'], '["!", "foo"]'], + [['foo', '!', 'ccc'], '["foo", "!", "ccc"]'], + [['foo' => '!'], '{foo: "!"}'], + [['foo' => ' !'], '{foo: " !"}'], + [['foo' => '!', 'bar' => 'ccc'], '{foo: "!", bar: "ccc"}'], + [['bar' => 'ccc', 'foo' => '! '], '{bar: "ccc", foo: "! "}'], + ['!]]]', '"!]]]"'], + ['!}', '"!}"'], + ['!,}foo,]', '"!,}foo,]"'], + [['!'], '! ["!"]'], + ]; + } } diff --git a/src/Symfony/Contracts/Cache/LICENSE b/src/Symfony/Contracts/Cache/LICENSE index 3f853aaf35fe1..69d925ba7511e 100644 --- a/src/Symfony/Contracts/Cache/LICENSE +++ b/src/Symfony/Contracts/Cache/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 Fabien Potencier +Copyright (c) 2018-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Contracts/EventDispatcher/LICENSE b/src/Symfony/Contracts/EventDispatcher/LICENSE index 3f853aaf35fe1..69d925ba7511e 100644 --- a/src/Symfony/Contracts/EventDispatcher/LICENSE +++ b/src/Symfony/Contracts/EventDispatcher/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 Fabien Potencier +Copyright (c) 2018-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Contracts/HttpClient/LICENSE b/src/Symfony/Contracts/HttpClient/LICENSE index 3f853aaf35fe1..69d925ba7511e 100644 --- a/src/Symfony/Contracts/HttpClient/LICENSE +++ b/src/Symfony/Contracts/HttpClient/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 Fabien Potencier +Copyright (c) 2018-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Contracts/LICENSE b/src/Symfony/Contracts/LICENSE index 3f853aaf35fe1..69d925ba7511e 100644 --- a/src/Symfony/Contracts/LICENSE +++ b/src/Symfony/Contracts/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 Fabien Potencier +Copyright (c) 2018-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Contracts/Service/LICENSE b/src/Symfony/Contracts/Service/LICENSE index 3f853aaf35fe1..69d925ba7511e 100644 --- a/src/Symfony/Contracts/Service/LICENSE +++ b/src/Symfony/Contracts/Service/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 Fabien Potencier +Copyright (c) 2018-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Contracts/Translation/LICENSE b/src/Symfony/Contracts/Translation/LICENSE index 3f853aaf35fe1..69d925ba7511e 100644 --- a/src/Symfony/Contracts/Translation/LICENSE +++ b/src/Symfony/Contracts/Translation/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2019 Fabien Potencier +Copyright (c) 2018-2020 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal