8000 From 8fc3dcc45d37ba5daaea5549d228cfd10adfb506 Mon Sep 17 00:00:00 2001 From: Alexander Grimalovsky Date: Sun, 22 Jan 2023 15:38:39 +0300 Subject: [PATCH 01/38] [Messenger] Respect `isRetryable` decision of the retry strategy when deciding if failed message should be re-delivered --- .../SendFailedMessageForRetryListener.php | 7 +++--- .../SendFailedMessageForRetryListenerTest.php | 23 ++++++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php index dab74b203f795..ddd7f1b1f61b0 100644 --- a/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php +++ b/src/Symfony/Component/Messenger/EventListener/SendFailedMessageForRetryListener.php @@ -123,7 +123,8 @@ public static function getSubscribedEvents() private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInterface $retryStrategy): bool { - if ($e instanceof RecoverableExceptionInterface) { + $isRetryable = $retryStrategy->isRetryable($envelope, $e); + if ($isRetryable && $e instanceof RecoverableExceptionInterface) { return true; } @@ -132,7 +133,7 @@ private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInt if ($e instanceof HandlerFailedException) { $shouldNotRetry = true; foreach ($e->getNestedExceptions() as $nestedException) { - if ($nestedException instanceof RecoverableExceptionInterface) { + if ($isRetryable && $nestedException instanceof RecoverableExceptionInterface) { return true; } @@ -150,7 +151,7 @@ private function shouldRetry(\Throwable $e, Envelope $envelope, RetryStrategyInt return false; } - return $retryStrategy->isRetryable($envelope, $e); + return $isRetryable; } private function getRetryStrategyForTransport(string $alias): ?RetryStrategyInterface diff --git a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php index a5fe10e85578b..8d795d7b86c23 100644 --- a/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php +++ b/src/Symfony/Component/Messenger/Tests/EventListener/SendFailedMessageForRetryListenerTest.php @@ -63,7 +63,7 @@ public function testRecoverableStrategyCausesRetry() $senderLocator->expects($this->once())->method('has')->willReturn(true); $senderLocator->expects($this->once())->method('get')->willReturn($sender); $retryStategy = $this->createMock(RetryStrategyInterface::class); - $retryStategy->expects($this->never())->method('isRetryable'); + $retryStategy->expects($this->once())->method('isRetryable')->willReturn(true); $retryStategy->expects($this->once())->method('getWaitingTime')->willReturn(1000); $retryStrategyLocator = $this->createMock(ContainerInterface::class); $retryStrategyLocator->expects($this->once())->method('has')->willReturn(true); @@ -78,6 +78,27 @@ public function testRecoverableStrategyCausesRetry() $listener->onMessageFailed($event); } + public function testRetryIsOnlyAllowedWhenPermittedByRetryStrategy() + { + $senderLocator = $this->createMock(ContainerInterface::class); + $senderLocator->expects($this->never())->method('has'); + $senderLocator->expects($this->never())->method('get'); + $retryStrategy = $this->createMock(RetryStrategyInterface::class); + $retryStrategy->expects($this->once())->method('isRetryable')->willReturn(false); + $retryStrategy->expects($this->never())->method('getWaitingTime'); + $retryStrategyLocator = $this->createMock(ContainerInterface::class); + $retryStrategyLocator->expects($this->once())->method('has')->willReturn(true); + $retryStrategyLocator->expects($this->once())->method('get')->willReturn($retryStrategy); + + $listener = new SendFailedMessageForRetryListener($senderLocator, $retryStrategyLocator); + + $exception = new RecoverableMessageHandlingException('retry'); + $envelope = new Envelope(new \stdClass()); + $event = new WorkerMessageFailedEvent($envelope, 'my_receiver', $exception); + + $listener->onMessageFailed($event); + } + public function testEnvelopeIsSentToTransportOnRetry() { $exception = new \Exception('no!'); From 6d5400a5d9ee0c20f4439f7161a81eed58add375 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 28 Feb 2023 14:26:05 +0100 Subject: [PATCH 02/38] [PropertyInfo] Fix phpDocExtractor nullable array value type --- .../Tests/Extractor/PhpDocExtractorTest.php | 4 ++- .../Extractor/ReflectionExtractorTest.php | 6 ++++ .../PropertyInfo/Tests/Fixtures/Dummy.php | 10 +++++++ .../PropertyInfo/Util/PhpDocTypeHelper.php | 30 ++++++------------- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php index c6a02b5f2f3e4..b3489d9fb0c10 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php @@ -169,7 +169,7 @@ public function testExtractCollection($property, array $type = null, $shortDescr public static function provideCollectionTypes() { return [ - ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, null, new Type(Type::BUILTIN_TYPE_STRING))], null, null], + ['iteratorCollection', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, [new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_INT)], new Type(Type::BUILTIN_TYPE_STRING))], null, null], ['iteratorCollectionWithKey', [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Iterator', true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))], null, null], [ 'nestedIterators', @@ -265,6 +265,8 @@ public static function typesWithCustomPrefixesProvider() ['i', [new Type(Type::BUILTIN_TYPE_STRING, true), new Type(Type::BUILTIN_TYPE_INT, true)], null, null], ['j', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'DateTime')], null, null], ['nullableCollectionOfNonNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, false))], null, null], + ['nonNullableCollectionOfNullableElements', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_INT, true))], null, null], + ['nullableCollectionOfMultipleNonNullableElementTypes', [new Type(Type::BUILTIN_TYPE_ARRAY, true, null, true, new Type(Type::BUILTIN_TYPE_INT), [new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)])], null, null], ['donotexist', null, null, null], ['staticGetter', null, null, null], ['staticSetter', null, null, null], diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php index b7955584d8c36..5f81dd5a65186 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php @@ -62,6 +62,8 @@ public function testGetProperties() 'i', 'j', 'nullableCollectionOfNonNullableElements', + 'nonNullableCollectionOfNullableElements', + 'nullableCollectionOfMultipleNonNullableElementTypes', 'emptyVar', 'iteratorCollection', 'iteratorCollectionWithKey', @@ -124,6 +126,8 @@ public function testGetPropertiesWithCustomPrefixes() 'i', 'j', 'nullableCollectionOfNonNullableElements', + 'nonNullableCollectionOfNullableElements', + 'nullableCollectionOfMultipleNonNullableElementTypes', 'emptyVar', 'iteratorCollection', 'iteratorCollectionWithKey', @@ -175,6 +179,8 @@ public function testGetPropertiesWithNoPrefixes() 'i', 'j', 'nullableCollectionOfNonNullableElements', + 'nonNullableCollectionOfNullableElements', + 'nullableCollectionOfMultipleNonNullableElementTypes', 'emptyVar', 'iteratorCollection', 'iteratorCollectionWithKey', diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php index 8d956a1103fc0..2fb3d2e0f807c 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php @@ -98,6 +98,16 @@ class Dummy extends ParentDummy */ public $nullableCollectionOfNonNullableElements; + /** + * @var array + */ + public $nonNullableCollectionOfNullableElements; + + /** + * @var null|array + */ + public $nullableCollectionOfMultipleNonNullableElementTypes; + /** * @var array */ diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php index 2c858c3bf9f8b..44a4614985563 100644 --- a/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php +++ b/src/Symfony/Component/PropertyInfo/Util/PhpDocTypeHelper.php @@ -115,15 +115,10 @@ private function createType(DocType $type, bool $nullable, string $docType = nul [$phpType, $class] = $this->getPhpTypeAndClass((string) $fqsen); - $key = $this->getTypes($type->getKeyType()); - $value = $this->getTypes($type->getValueType()); + $keys = $this->getTypes($type->getKeyType()); + $values = $this->getTypes($type->getValueType()); - // More than 1 type returned means it is a Compound type, which is - // not handled by Type, so better use a null value. - $key = 1 === \count($key) ? $key[0] : null; - $value = 1 === \count($value) ? $value[0] : null; - - return new Type($phpType, $nullable, $class, true, $key, $value); + return new Type($phpType, $nullable, $class, true, $keys, $values); } // Cannot guess @@ -131,27 +126,20 @@ private function createType(DocType $type, bool $nullable, string $docType = nul return null; } - if (str_ends_with($docType, '[]')) { - $collectionKeyType = new Type(Type::BUILTIN_TYPE_INT); - $collectionValueType = $this->createType($type, false, substr($docType, 0, -2)); + if (str_ends_with($docType, '[]') && $type instanceof Array_) { + $collectionKeyTypes = new Type(Type::BUILTIN_TYPE_INT); + $collectionValueTypes = $this->getTypes($type->getValueType()); - return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType); + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); } if ((str_starts_with($docType, 'list<') || str_starts_with($docType, 'array<')) && $type instanceof Array_) { // array is converted to x[] which is handled above // so it's only necessary to handle array here - $collectionKeyType = $this->getTypes($type->getKeyType())[0]; - + $collectionKeyTypes = $this->getTypes($type->getKeyType()); $collectionValueTypes = $this->getTypes($type->getValueType()); - if (1 != \count($collectionValueTypes)) { - // the Type class does not support union types yet, so assume that no type was defined - $collectionValueType = null; - } else { - $collectionValueType = $collectionValueTypes[0]; - } - return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyType, $collectionValueType); + return new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, $collectionKeyTypes, $collectionValueTypes); } $docType = $this->normalizeType($docType); From c6a452ddc7cd838b0c21ec28507e01fee413022e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 28 Apr 2023 15:33:20 +0200 Subject: [PATCH 03/38] Bump Symfony version to 5.4.24 --- src/Symfony/Component/HttpKernel/Kernel.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 7006a45cf2f49..1173f499d023f 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,12 +78,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static $freshCache = []; - public const VERSION = '5.4.23'; - public const VERSION_ID = 50423; + public const VERSION = '5.4.24-DEV'; + public const VERSION_ID = 50424; public const MAJOR_VERSION = 5; public const MINOR_VERSION = 4; - public const RELEASE_VERSION = 23; - public const EXTRA_VERSION = ''; + public const RELEASE_VERSION = 24; + public const EXTRA_VERSION = 'DEV'; public const END_OF_MAINTENANCE = '11/2024'; public const END_OF_LIFE = '11/2025'; From dfd7a5ba7736f0000d3ea171a56fb57cd98f7ae9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 2 May 2023 12:33:25 +0200 Subject: [PATCH 04/38] [Messenger] Fix registering message handlers --- .../Component/Messenger/DependencyInjection/MessengerPass.php | 2 +- .../Messenger/Tests/DependencyInjection/MessengerPassTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php index f423889713972..4a4e00c81dcbf 100644 --- a/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php +++ b/src/Symfony/Component/Messenger/DependencyInjection/MessengerPass.php @@ -143,7 +143,7 @@ private function registerHandlers(ContainerBuilder $container, array $busIds) } if ('__invoke' !== $method) { - $wrapperDefinition = (new Definition('callable'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable'); + $wrapperDefinition = (new Definition('Closure'))->addArgument([new Reference($serviceId), $method])->setFactory('Closure::fromCallable'); $definitions[$definitionId = '.messenger.method_on_object_wrapper.'.ContainerBuilder::hash($message.':'.$priority.':'.$serviceId.':'.$method)] = $wrapperDefinition; } else { diff --git a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php index 3d29497d34c1a..3411b0bbef482 100644 --- a/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php +++ b/src/Symfony/Component/Messenger/Tests/DependencyInjection/MessengerPassTest.php @@ -255,7 +255,7 @@ public function testGetClassesAndMethodsAndPrioritiesFromTheSubscriber() $dummyHandlerReference = $dummyHandlerDescriptorDefinition->getArgument(0); $dummyHandlerDefinition = $container->getDefinition($dummyHandlerReference); - $this->assertSame('callable', $dummyHandlerDefinition->getClass()); + $this->assertSame('Closure', $dummyHandlerDefinition->getClass()); $this->assertEquals([new Reference(HandlerMappingMethods::class), 'dummyMethod'], $dummyHandlerDefinition->getArgument(0)); $this->assertSame(['Closure', 'fromCallable'], $dummyHandlerDefinition->getFactory()); From 159bf0bd487a575c22c0df7bb0e3b59dd5370b48 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 2 May 2023 12:45:29 +0200 Subject: [PATCH 05/38] [ErrorHandler] Skip Httplug deprecations for HttplugClient --- src/Symfony/Component/ErrorHandler/DebugClassLoader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php index e19223d3f3c53..844dbd6d23e85 100644 --- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -416,7 +416,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array $this->checkClass($use); } if (isset(self::$deprecated[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && !isset(self::$deprecated[$class]) - && !(HttplugClient::class === $class && \in_array($use, [\Http\Message\RequestFactory::class, \Http\Message\StreamFactory::class, \Http\Message\UriFactory::class], true)) + && !(HttplugClient::class === $class && \in_array($use, [\Http\Client\HttpClient::class, \Http\Message\RequestFactory::class, \Http\Message\StreamFactory::class, \Http\Message\UriFactory::class], true)) ) { $type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait'); $verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses'); From 3683d73416880dddf20ec49f94a23089fa195679 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 2 May 2023 15:01:16 +0200 Subject: [PATCH 06/38] [HttpClient] Dev-require php-http/message-factory --- composer.json | 1 + src/Symfony/Component/HttpClient/HttplugClient.php | 2 +- src/Symfony/Component/HttpClient/composer.json | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d5e8a209d3be0..2f20a572b9eb7 100644 --- a/composer.json +++ b/composer.json @@ -136,6 +136,7 @@ "nyholm/psr7": "^1.0", "pda/pheanstalk": "^4.0", "php-http/httplug": "^1.0|^2.0", + "php-http/message-factory": "^1.0", "phpstan/phpdoc-parser": "^1.0", "predis/predis": "~1.1", "psr/http-client": "^1.0", diff --git a/src/Symfony/Component/HttpClient/HttplugClient.php b/src/Symfony/Component/HttpClient/HttplugClient.php index 491ea9e4033b7..2d9eec30f1238 100644 --- a/src/Symfony/Component/HttpClient/HttplugClient.php +++ b/src/Symfony/Component/HttpClient/HttplugClient.php @@ -47,7 +47,7 @@ } if (!interface_exists(RequestFactory::class)) { - throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require nyholm/psr7".'); + throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory".'); } /** diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json index 57d31c12dfd8c..7f546b3a23981 100644 --- a/src/Symfony/Component/HttpClient/composer.json +++ b/src/Symfony/Component/HttpClient/composer.json @@ -38,6 +38,7 @@ "guzzlehttp/promises": "^1.4", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", + "php-http/message-factory": "^1.0", "psr/http-client": "^1.0", "symfony/dependency-injection": "^4.4|^5.0|^6.0", "symfony/http-kernel": "^4.4.13|^5.1.5|^6.0", From e331b288fb9650cd62c78117046584128ebba77e Mon Sep 17 00:00:00 2001 From: MatTheCat Date: Tue, 2 May 2023 18:13:31 +0200 Subject: [PATCH 07/38] =?UTF-8?q?Do=20not=20expose=20`Sfjs`=20as=20it=20is?= =?UTF-8?q?=20unused=20and=20conflicts=20with=20WebProfilerBundle=E2=80=99?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resources/assets/js/exception.js | 494 +++++++++--------- 1 file changed, 241 insertions(+), 253 deletions(-) diff --git a/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js b/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js index a85409da3cc89..95b8ea17197c9 100644 --- a/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js +++ b/src/Symfony/Component/ErrorHandler/Resources/assets/js/exception.js @@ -1,297 +1,285 @@ /* This file is based on WebProfilerBundle/Resources/views/Profiler/base_js.html.twig. If you make any change in this file, verify the same change is needed in the other file. */ /* .tab'); + var tabNavigation = document.createElement('ul'); + tabNavigation.className = 'tab-navigation'; + + var selectedTabId = 'tab-' + i + '-0'; /* select the first tab by default */ + for (var j = 0; j < tabs.length; j++) { + var tabId = 'tab-' + i + '-' + j; + var tabTitle = tabs[j].querySelector('.tab-title').innerHTML; + + var tabNavigationItem = document.createElement('li'); + tabNavigationItem.setAttribute('data-tab-id', tabId); + if (hasClass(tabs[j], 'active')) { selectedTabId = tabId; } + if (hasClass(tabs[j], 'disabled')) { addClass(tabNavigationItem, 'disabled'); } + tabNavigationItem.innerHTML = tabTitle; + tabNavigation.appendChild(tabNavigationItem); + + var tabContent = tabs[j].querySelector('.tab-content'); + tabContent.parentElement.setAttribute('id', tabId); + } - if (navigator.clipboard) { - document.querySelectorAll('[data-clipboard-text]').forEach(function(element) { - removeClass(element, 'hidden'); - element.addEventListener('click', function() { - navigator.clipboard.writeText(element.getAttribute('data-clipboard-text')); - }) - }); + tabGroups[i].insertBefore(tabNavigation, tabGroups[i].firstChild); + addClass(document.querySelector('[data-tab-id="' + selectedTabId + '"]'), 'active'); } - return { - addEventListener: addEventListener, + /* display the active tab and add the 'click' event listeners */ + for (i = 0; i < tabGroups.length; i++) { + tabNavigation = tabGroups[i].querySelectorAll(':scope >.tab-navigation li'); - createTabs: function() { - var tabGroups = document.querySelectorAll('.sf-tabs:not([data-processed=true])'); + for (j = 0; j < tabNavigation.length; j++) { + tabId = tabNavigation[j].getAttribute('data-tab-id'); + document.getElementById(tabId).querySelector('.tab-title').className = 'hidden'; - /* create the tab navigation for each group of tabs */ - for (var i = 0; i < tabGroups.length; i++) { - var tabs = tabGroups[i].querySelectorAll(':scope > .tab'); - var tabNavigation = document.createElement('ul'); - tabNavigation.className = 'tab-navigation'; - - var selectedTabId = 'tab-' + i + '-0'; /* select the first tab by default */ - for (var j = 0; j < tabs.length; j++) { - var tabId = 'tab-' + i + '-' + j; - var tabTitle = tabs[j].querySelector('.tab-title').innerHTML; + if (hasClass(tabNavigation[j], 'active')) { + document.getElementById(tabId).className = 'block'; + } else { + document.getElementById(tabId).className = 'hidden'; + } - var tabNavigationItem = document.createElement('li'); - tabNavigationItem.setAttribute('data-tab-id', tabId); - if (hasClass(tabs[j], 'active')) { selectedTabId = tabId; } - if (hasClass(tabs[j], 'disabled')) { addClass(tabNavigationItem, 'disabled'); } - tabNavigationItem.innerHTML = tabTitle; - tabNavigation.appendChild(tabNavigationItem); + tabNavigation[j].addEventListener('click', function(e) { + var activeTab = e.target || e.srcElement; - var tabContent = tabs[j].querySelector('.tab-content'); - tabContent.parentElement.setAttribute('id', tabId); + /* needed because when the tab contains HTML contents, user can click */ + /* on any of those elements instead of their parent '
  • ' element */ + while (activeTab.tagName.toLowerCase() !== 'li') { + activeTab = activeTab.parentNode; } - tabGroups[i].insertBefore(tabNavigation, tabGroups[i].firstChild); - addClass(document.querySelector('[data-tab-id="' + selectedTabId + '"]'), 'active'); - } + /* get the full list of tabs through the parent of the active tab element */ + var tabNavigation = activeTab.parentNode.children; + for (var k = 0; k < tabNavigation.length; k++) { + var tabId = tabNavigation[k].getAttribute('data-tab-id'); + document.getElementById(tabId).className = 'hidden'; + removeClass(tabNavigation[k], 'active'); + } - /* display the active tab and add the 'click' event listeners */ - for (i = 0; i < tabGroups.length; i++) { - tabNavigation = tabGroups[i].querySelectorAll(':scope >.tab-navigation li'); + addClass(activeTab, 'active'); + var activeTabId = activeTab.getAttribute('data-tab-id'); + document.getElementById(activeTabId).className = 'block'; + }); + } - for (j = 0; j < tabNavigation.length; j++) { - tabId = tabNavigation[j].getAttribute('data-tab-id'); - document.getElementById(tabId).querySelector('.tab-title').className = 'hidden'; + tabGroups[i].setAttribute('data-processed', 'true'); + } + })(); - if (hasClass(tabNavigation[j], 'active')) { - document.getElementById(tabId).className = 'block'; - } else { - document.getElementById(tabId).className = 'hidden'; - } + (function createToggles() { + var toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])'); - tabNavigation[j].addEventListener('click', function(e) { - var activeTab = e.target || e.srcElement; + for (var i = 0; i < toggles.length; i++) { + var elementSelector = toggles[i].getAttribute('data-toggle-selector'); + var element = document.querySelector(elementSelector); - /* needed because when the tab contains HTML contents, user can click */ - /* on any of those elements instead of their parent '
  • ' element */ - while (activeTab.tagName.toLowerCase() !== 'li') { - activeTab = activeTab.parentNode; - } + addClass(element, 'sf-toggle-content'); - /* get the full list of tabs through the parent of the active tab element */ - var tabNavigation = activeTab.parentNode.children; - for (var k = 0; k < tabNavigation.length; k++) { - var tabId = tabNavigation[k].getAttribute('data-tab-id'); - document.getElementById(tabId).className = 'hidden'; - removeClass 8000 (tabNavigation[k], 'active'); - } + if (toggles[i].hasAttribute('data-toggle-initial') && toggles[i].getAttribute('data-toggle-initial') == 'display') { + addClass(toggles[i], 'sf-toggle-on'); + addClass(element, 'sf-toggle-visible'); + } else { + addClass(toggles[i], 'sf-toggle-off'); + addClass(element, 'sf-toggle-hidden'); + } - addClass(activeTab, 'active'); - var activeTabId = activeTab.getAttribute('data-tab-id'); - document.getElementById(activeTabId).className = 'block'; - }); - } + addEventListener(toggles[i], 'click', function(e) { + e.preventDefault(); - tabGroups[i].setAttribute('data-processed', 'true'); + if ('' !== window.getSelection().toString()) { + /* Don't do anything on text selection */ + return; } - }, - - createToggles: function() { - var toggles = document.querySelectorAll('.sf-toggle:not([data-processed=true])'); - - for (var i = 0; i < toggles.length; i++) { - var elementSelector = toggles[i].getAttribute('data-toggle-selector'); - var element = document.querySelector(elementSelector); - - addClass(element, 'sf-toggle-content'); - - if (toggles[i].hasAttribute('data-toggle-initial') && toggles[i].getAttribute('data-toggle-initial') == 'display') { - addClass(toggles[i], 'sf-toggle-on'); - addClass(element, 'sf-toggle-visible'); - } else { - addClass(toggles[i], 'sf-toggle-off'); - addClass(element, 'sf-toggle-hidden'); - } - - addEventListener(toggles[i], 'click', function(e) { - e.preventDefault(); - if ('' !== window.getSelection().toString()) { - /* Don't do anything on text selection */ - return; - } + var toggle = e.target || e.srcElement; - var toggle = e.target || e.srcElement; + /* needed because when the toggle contains HTML contents, user can click */ + /* on any of those elements instead of their parent '.sf-toggle' element */ + while (!hasClass(toggle, 'sf-toggle')) { + toggle = toggle.parentNode; + } - /* needed because when the toggle contains HTML contents, user can click */ - /* on any of those elements instead of their parent '.sf-toggle' element */ - while (!hasClass(toggle, 'sf-toggle')) { - toggle = toggle.parentNode; - } + var element = document.querySelector(toggle.getAttribute('data-toggle-selector')); - var element = document.querySelector(toggle.getAttribute('data-toggle-selector')); + toggleClass(toggle, 'sf-toggle-on'); + toggleClass(toggle, 'sf-toggle-off'); + toggleClass(element, 'sf-toggle-hidden'); + toggleClass(element, 'sf-toggle-visible'); - toggleClass(toggle, 'sf-toggle-on'); - toggleClass(toggle, 'sf-toggle-off'); - toggleClass(element, 'sf-toggle-hidden'); - toggleClass(element, 'sf-toggle-visible'); + /* the toggle doesn't change its contents when clicking on it */ + if (!toggle.hasAttribute('data-toggle-alt-content')) { + return; + } - /* the toggle doesn't change its contents when clicking on it */ - if (!toggle.hasAttribute('data-toggle-alt-content')) { - return; - } + if (!toggle.hasAttribute('data-toggle-original-content')) { + toggle.setAttribute('data-toggle-original-content', toggle.innerHTML); + } - if (!toggle.hasAttribute('data-toggle-original-content')) { - toggle.setAttribute('data-toggle-original-content', toggle.innerHTML); - } + var currentContent = toggle.innerHTML; + var originalContent = toggle.getAttribute('data-toggle-original-content'); + var altContent = toggle.getAttribute('data-toggle-alt-content'); + toggle.innerHTML = currentContent !== altContent ? altContent : originalContent; + }); - var currentContent = toggle.innerHTML; - var originalContent = toggle.getAttribute('data-toggle-original-content'); - var altContent = toggle.getAttribute('data-toggle-alt-content'); - toggle.innerHTML = currentContent !== altContent ? altContent : originalContent; - }); + /* Prevents from disallowing clicks on links inside toggles */ + var toggleLinks = toggles[i].querySelectorAll('a'); + for (var j = 0; j < toggleLinks.length; j++) { + addEventListener(toggleLinks[j], 'click', function(e) { + e.stopPropagation(); + }); + } - /* Prevents from disallowing clicks on links inside toggles */ - var toggleLinks = toggles[i].querySelectorAll('a'); - for (var j = 0; j < toggleLinks.length; j++) { - addEventListener(toggleLinks[j], 'click', function(e) { - e.stopPropagation(); - }); - } + /* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */ + var copyToClipboardElements = toggles[i].querySelectorAll('span[data-clipboard-text]'); + for (var k = 0; k < copyToClipboardElements.length; k++) { + addEventListener(copyToClipboardElements[k], 'click', function(e) { + e.stopPropagation(); + }); + } - /* Prevents from disallowing clicks on "copy to clipboard" elements inside toggles */ - var copyToClipboardElements = toggles[i].querySelectorAll('span[data-clipboard-text]'); - for (var k = 0; k < copyToClipboardElements.length; k++) { - addEventListener(copyToClipboardElements[k], 'click', function(e) { - e.stopPropagation(); - }); - } + toggles[i].setAttribute('data-processed', 'true'); + } + })(); - toggles[i].setAttribute('data-processed', 'true'); + (function createFilters() { + document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) { + var filters = filter.closest('[data-filters]'), + type = 'choice', + name = filter.dataset.filter, + ucName = name.charAt(0).toUpperCase()+name.slice(1), + list = document.createElement('ul'), + values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'), + labels = {}, + defaults = null, + indexed = {}, + processed = {}; + if (typeof values === 'string') { + type = 'level'; + labels = values.split(','); + values = values.toLowerCase().split(','); + defaults = values.length - 1; + } + addClass(list, 'filter-list'); + addClass(list, 'filter-list-'+type); + values.forEach(function (value, i) { + if (value instanceof HTMLElement) { + value = value.dataset['filter'+ucName]; } - }, - - createFilters: function() { - document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) { - var filters = filter.closest('[data-filters]'), - type = 'choice', - name = filter.dataset.filter, - ucName = name.charAt(0).toUpperCase()+name.slice(1), - list = document.createElement('ul'), - values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'), - labels = {}, - defaults = null, - indexed = {}, - processed = {}; - if (typeof values === 'string') { - type = 'level'; - labels = values.split(','); - values = values.toLowerCase().split(','); - defaults = values.length - 1; - } - addClass(list, 'filter-list'); - addClass(list, 'filter-list-'+type); - values.forEach(function (value, i) { - if (value instanceof HTMLElement) { - value = value.dataset['filter'+ucName]; - } - if (value in processed) { + if (value in processed) { + return; + } + var option = document.createElement('li'), + label = i in labels ? labels[i] : value, + active = false, + matches; + if ('' === label) { + option.innerHTML = '(none)'; + } else { + option.innerText = label; + } + option.dataset.filter = value; + option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows'); + indexed[value] = i; + list.appendChild(option); + addEventListener(option, 'click', function () { + if ('choice' === type) { + filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { + if (option.dataset.filter === row.dataset['filter'+ucName]) { + toggleClass(row, 'filter-hidden-'+name); + } + }); + toggleClass(option, 'active'); + } else if ('level' === type) { + if (i === this.parentNode.querySelectorAll('.active').length - 1) { return; } - var option = document.createElement('li'), - label = i in labels ? labels[i] : value, - active = false, - matches; - if ('' === label) { - option.innerHTML = '(none)'; - } else { - option.innerText = label; - } - option.dataset.filter = value; - option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows'); - indexed[value] = i; - list.appendChild(option); - addEventListener(option, 'click', function () { - if ('choice' === type) { - filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { - if (option.dataset.filter === row.dataset['filter'+ucName]) { - toggleClass(row, 'filter-hidden-'+name); - } - }); - toggleClass(option, 'active'); - } else if ('level' === type) { - if (i === this.parentNode.querySelectorAll('.active').length - 1) { - return; + this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) { + if (j <= i) { + addClass(currentOption, 'active'); + if (i === j) { + addClass(currentOption, 'last-active'); + } else { + removeClass(currentOption, 'last-active'); } - this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) { - if (j <= i) { - addClass(currentOption, 'active'); - if (i === j) { - addClass(currentOption, 'last-active'); - } else { - removeClass(currentOption, 'last-active'); - } - } else { - removeClass(currentOption, 'active'); - removeClass(currentOption, 'last-active'); - } - }); - filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { - if (i < indexed[row.dataset['filter'+ucName]]) { - addClass(row, 'filter-hidden-'+name); - } else { - removeClass(row, 'filter-hidden-'+name); - } - }); + } else { + removeClass(currentOption, 'active'); + removeClass(currentOption, 'last-active'); } }); - if ('choice' === type) { - active = null === defaults || 0 <= defaults.indexOf(value); - } else if ('level' === type) { - active = i <= defaults; - if (active && i === defaults) { - addClass(option, 'last-active'); + filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { + if (i < indexed[row.dataset['filter'+ucName]]) { + addClass(row, 'filter-hidden-'+name); + } else { + removeClass(row, 'filter-hidden-'+name); } - } - if (active) { - addClass(option, 'active'); - } else { - filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) { - toggleClass(row, 'filter-hidden-'+name); - }); - } - processed[value] = true; - }); - - if (1 < list.childNodes.length) { - filter.appendChild(list); - filter.dataset.filtered = ''; + }); } }); + if ('choice' === type) { + active = null === defaults || 0 <= defaults.indexOf(value); + } else if ('level' === type) { + active = i <= defaults; + if (active && i === defaults) { + addClass(option, 'last-active'); + } + } + if (active) { + addClass(option, 'active'); + } else { + filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) { + toggleClass(row, 'filter-hidden-'+name); + }); + } + processed[value] = true; + }); + + if (1 < list.childNodes.length) { + filter.appendChild(list); + filter.dataset.filtered = ''; } - }; + }); })(); - - Sfjs.addEventListener(document, 'DOMContentLoaded', function() { - Sfjs.createTabs(); - Sfjs.createToggles(); - Sfjs.createFilters(); - }); -} +})(); /*]]>*/ From 98a8aa22dee799da57fa9c9d3145ae36fd1b6717 Mon Sep 17 00:00:00 2001 From: MatTheCat Date: Tue, 2 May 2023 18:38:36 +0200 Subject: [PATCH 08/38] Remove legacy filters remnants --- .../views/Collector/translation.html.twig | 10 +++--- .../views/Profiler/profiler.css.twig | 36 ------------------- 2 files changed, 4 insertions(+), 42 deletions(-) diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig index a8a5c273656b5..f48c9fce7c4df 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig @@ -155,22 +155,20 @@ - - {% endblock messages %} {% endif %} {% endblock %} {% macro render_table(messages, is_fallback) %} - +
    - + {% if is_fallback %} {% endif %} - + @@ -178,7 +176,7 @@ {% for message in messages %} - + {% if is_fallback %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 24ec0863d6117..2c59150b5a345 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -963,42 +963,6 @@ tr.status-warning td { display: block; } -{# Filters - ========================================================================= #} -[data-filters] { position: relative; } -[data-filtered] { cursor: pointer; } -[data-filtered]:after { content: '\00a0\25BE'; } -[data-filtered]:hover .filter-list li { display: inline-flex; } -[class*="filter-hidden-"] { display: none; } -.filter-list { position: absolute; border: var(--border); box-shadow: var(--shadow); margin: 0; padding: 0; display: flex; flex-direction: column; } -.filter-list :after { content: ''; } -.filter-list li { - background: var(--tab-disabled-background); - border-bottom: var(--border); - color: var(--tab-disabled-color); - display: none; - list-style: none; - margin: 0; - padding: 5px 10px; - text-align: left; - font-weight: normal; -} -.filter-list li.active { - background: var(--tab-background); - color: var(--tab-color); -} -.filter-list li.last-active { - background: var(--tab-active-background); - color: var(--tab-active-color); -} - -.filter-list-level li { cursor: s-resize; } -.filter-list-level li.active { cursor: n-resize; } -.filter-list-level li.last-active { cursor: default; } -.filter-list-level li.last-active:before { content: '\2714\00a0'; } -.filter-list-choice li:before { content: '\2714\00a0'; color: transparent; } -.filter-list-choice li.active:before { color: unset; } - {# Twig panel ========================================================================= #} #twig-dump pre { From 554949391ab8abd56a2563a6f47128c91bd82b62 Mon Sep 17 00:00:00 2001 From: Allison Guilhem Date: Fri, 28 Apr 2023 23:05:15 +0200 Subject: [PATCH 09/38] [Serializer] Throw NotNormalizableValueException if it doesn't concern a backedEnum in construct method --- .../Normalizer/AbstractNormalizer.php | 3 ++ .../Normalizer/BackedEnumNormalizer.php | 6 ++- .../Fixtures/DummyObjectWithEnumProperty.php | 10 ++++ .../Normalizer/BackedEnumNormalizerTest.php | 2 +- .../Serializer/Tests/SerializerTest.php | 49 ++++++++++++++++++- 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php index efd8cbb567637..bd41b8da6fa72 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractNormalizer.php @@ -342,6 +342,7 @@ protected function instantiateObject(array &$data, string $class, array &$contex $constructor = $this->getConstructor($data, $class, $context, $reflectionClass, $allowedAttributes); if ($constructor) { + $context['has_constructor'] = true; if (true !== $constructor->isPublic()) { return $reflectionClass->newInstanceWithoutConstructor(); } @@ -431,6 +432,8 @@ protected function instantiateObject(array &$data, string $class, array &$contex } } + unset($context['has_constructor']); + return new $class(); } diff --git a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php index 21fac3248cd6e..e7efb0057c09f 100644 --- a/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/BackedEnumNormalizer.php @@ -64,7 +64,11 @@ public function denormalize($data, string $type, string $format = null, array $c try { return $type::from($data); } catch (\ValueError $e) { - throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type); + if (isset($context['has_constructor'])) { + throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type); + } + + throw NotNormalizableValueException::createForUnexpectedDataType('The data must belong to a backed enumeration of type '.$type, $data, [$type], $context['deserialization_path'] ?? null, true, 0, $e); } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php new file mode 100644 index 0000000000000..f2677195f2820 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyObjectWithEnumProperty.php @@ -0,0 +1,10 @@ +expectException(InvalidArgumentException::class); + $this->expectException(NotNormalizableValueException::class); $this->expectExceptionMessage('The data must belong to a backed enumeration of type '.StringBackedEnumDummy::class); $this->normalizer->denormalize('POST', StringBackedEnumDummy::class); diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index fc0b6cc5af876..b4e84132a0858 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -60,6 +60,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo; use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor; +use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumProperty; use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy; use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74Full; @@ -1230,7 +1231,51 @@ public function testCollectDenormalizationErrorsWithEnumConstructor() /** * @requires PHP 8.1 */ - public function testNoCollectDenormalizationErrorsWithWrongEnum() + public function testCollectDenormalizationErrorsWithWrongPropertyWithoutConstruct() + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader()); + $reflectionExtractor = new ReflectionExtractor(); + $propertyInfoExtractor = new PropertyInfoExtractor([], [$reflectionExtractor], [], [], []); + + $serializer = new Serializer( + [ + new BackedEnumNormalizer(), + new ObjectNormalizer($classMetadataFactory, null, null, $propertyInfoExtractor), + ], + ['json' => new JsonEncoder()] + ); + + try { + $serializer->deserialize('{"get": "POST"}', DummyObjectWithEnumProperty::class, 'json', [ + DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, + ]); + } catch (\Throwable $e) { + $this->assertInstanceOf(PartialDenormalizationException::class, $e); + } + + $exceptionsAsArray = array_map(function (NotNormalizableValueException $e): array { + return [ + 'currentType' => $e->getCurrentType(), + 'useMessageForUser' => $e->canUseMessageForUser(), + 'message' => $e->getMessage(), + ]; + }, $e->getErrors()); + + $expected = [ + [ + 'currentType' => 'string', + 'useMessageForUser' => true, + 'message' => 'The data must belong to a backed enumeration of type Symfony\Component\Serializer\Tests\Fixtures\StringBackedEnumDummy', + ], + ]; + + $this->assertSame($expected, $exceptionsAsArray); + } + + /** + * @requires PHP 8.1 + */ + public function testNoCollectDenormalizationErrorsWithWrongEnumOnConstructor() { $serializer = new Serializer( [ @@ -1241,7 +1286,7 @@ public function testNoCollectDenormalizationErrorsWithWrongEnum() ); try { - $serializer->deserialize('{"get": "invalid"}', DummyObjectWithEnumConstructor::class, 'json', [ + $serializer->deserialize('{"get": "POST"}', DummyObjectWithEnumConstructor::class, 'json', [ DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS => true, ]); } catch (\Throwable $th) { From f702e66369274b1d2b7c4d4af9ad3ee0f8f2476e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 3 May 2023 10:21:12 +0200 Subject: [PATCH 10/38] [HttpClient] Ensure HttplugClient ignores invalid HTTP headers --- composer.json | 1 + .../HttpClient/Internal/HttplugWaitLoop.php | 6 +++++- .../HttpClient/Tests/HttplugClientTest.php | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 2f20a572b9eb7..b291459895cb7 100644 --- a/composer.json +++ b/composer.json @@ -165,6 +165,7 @@ }, "config": { "allow-plugins": { + "php-http/discovery": false, "symfony/runtime": true } }, diff --git a/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php b/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php index 9f5658f560fbc..c61be22e34405 100644 --- a/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php +++ b/src/Symfony/Component/HttpClient/Internal/HttplugWaitLoop.php @@ -120,7 +120,11 @@ public function createPsr7Response(ResponseInterface $response, bool $buffer = f foreach ($response->getHeaders(false) as $name => $values) { foreach ($values as $value) { - $psrResponse = $psrResponse->withAddedHeader($name, $value); + try { + $psrResponse = $psrResponse->withAddedHeader($name, $value); + } catch (\InvalidArgumentException $e) { + // ignore invalid header + } } } diff --git a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php index 1f48be5c574c2..ba8fcbe3d68eb 100644 --- a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php +++ b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php @@ -267,4 +267,22 @@ function (\Exception $exception) use ($errorMessage, &$failureCallableCalled, $c $this->assertSame(200, $response->getStatusCode()); $this->assertSame('OK', (string) $response->getBody()); } + + public function testInvalidHeaderResponse() + { + $responseHeaders = [ + // space in header name not allowed in RFC 7230 + ' X-XSS-Protection' => '0', + 'Cache-Control' => 'no-cache', + ]; + $response = new MockResponse('body', ['response_headers' => $responseHeaders]); + $this->assertArrayHasKey(' x-xss-protection', $response->getHeaders()); + + $client = new HttplugClient(new MockHttpClient($response)); + $request = $client->createRequest('POST', 'http://localhost:8057/post') + ->withBody($client->createStream('foo=0123456789')); + + $resultResponse = $client->sendRequest($request); + $this->assertCount(1, $resultResponse->getHeaders()); + } } From ea449ca6bac80d0ea718fcf06e5fe6a9b97b329c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 13 Apr 2023 19:22:41 +0200 Subject: [PATCH 11/38] [HttpKernel] Don't use eval() to render ESI/SSI --- .../Component/HttpKernel/HttpCache/Esi.php | 16 ++++++---------- .../HttpKernel/HttpCache/HttpCache.php | 16 +++++++++++++++- .../Component/HttpKernel/HttpCache/Ssi.php | 14 ++++++-------- .../HttpKernel/Tests/HttpCache/EsiTest.php | 19 ++++++++++++------- .../HttpKernel/Tests/HttpCache/SsiTest.php | 9 ++++++--- 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php index cd6a00a 8000 10d61f..4d86508a42092 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php @@ -80,8 +80,10 @@ public function process(Request $request, Response $response) $content = preg_replace('#.*?#s', '', $content); $content = preg_replace('#]+>#s', '', $content); + static $cookie; + $cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true); + $boundary = base64_encode($cookie); $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); - $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]); $i = 1; while (isset($chunks[$i])) { @@ -95,16 +97,10 @@ public function process(Request $request, Response $response) throw new \RuntimeException('Unable to process an ESI tag without a "src" attribute.'); } - $chunks[$i] = sprintf('surrogate->handle($this, %s, %s, %s) ?>'."\n", - var_export($options['src'], true), - var_export($options['alt'] ?? '', true), - isset($options['onerror']) && 'continue' === $options['onerror'] ? 'true' : 'false' - ); - ++$i; - $chunks[$i] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[$i]); - ++$i; + $chunks[$i] = $boundary.$options['src']."\n".($options['alt'] ?? '')."\n".('continue' === ($options['onerror'] ?? ''))."\n"; + $i += 2; } - $content = implode('', $chunks); + $content = $boundary.implode('', $chunks).$boundary; $response->setContent($content); $response->headers->set('X-Body-Eval', 'ESI'); diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 5688fc0c13ccd..063d4105b160b 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -636,7 +636,21 @@ private function restoreResponseBody(Request $request, Response $response) if ($response->headers->has('X-Body-File')) { include $response->headers->get('X-Body-File'); } else { - eval('; ?>'.$response->getContent().'getContent(); + + if (substr($content, -24) === $boundary = substr($content, 0, 24)) { + $j = strpos($content, $boundary, 24); + echo substr($content, 24, $j - 24); + $i = $j + 24; + + while (false !== $j = strpos($content, $boundary, $i)) { + [$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4); + $i = $j + 24; + + echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors); + echo $part; + } + } } $response->setContent(ob_get_clean()); diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php index f114e05cfb2f6..bb48238ff1f4b 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php @@ -65,8 +65,10 @@ public function process(Request $request, Response $response) // we don't use a proper XML parser here as we can have SSI tags in a plain text response $content = $response->getContent(); + static $cookie; + $cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true); + $boundary = base64_encode($cookie); $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); - $chunks[0] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[0]); $i = 1; while (isset($chunks[$i])) { @@ -80,14 +82,10 @@ public function process(Request $request, Response $response) throw new \RuntimeException('Unable to process an SSI tag without a "virtual" attribute.'); } - $chunks[$i] = sprintf('surrogate->handle($this, %s, \'\', false) ?>'."\n", - var_export($options['virtual'], true) - ); - ++$i; - $chunks[$i] = str_replace($this->phpEscapeMap[0], $this->phpEscapeMap[1], $chunks[$i]); - ++$i; + $chunks[$i] = $boundary.$options['virtual']."\n\n\n"; + $i += 2; } - $content = implode('', $chunks); + $content = $boundary.implode('', $chunks).$boundary; $response->setContent($content); $response->headers->set('X-Body-Eval', 'SSI'); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php index 290bd94bdcb97..e876f28189087 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/EsiTest.php @@ -102,7 +102,7 @@ public function testMultilineEsiRemoveTagsAreRemoved() $response = new Response(' Keep this'." And this"); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals(' Keep this And this', $response->getContent()); + $this->assertEquals(' Keep this And this', substr($response->getContent(), 24, -24)); } public function testCommentTagsAreRemoved() @@ -113,7 +113,7 @@ public function testCommentTagsAreRemoved() $response = new Response(' Keep this'); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals(' Keep this', $response->getContent()); + $this->assertEquals(' Keep this', substr($response->getContent(), 24, -24)); } public function testProcess() @@ -124,23 +124,27 @@ public function testProcess() $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('foo surrogate->handle($this, \'...\', \'alt\', true) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "...\nalt\n1\n", ''], $content); $this->assertEquals('ESI', $response->headers->get('x-body-eval')); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('foo surrogate->handle($this, \'foo\\\'\', \'bar\\\'\', true) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "foo'\nbar'\n1\n", ''], $content); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content); $response = new Response('foo '); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content); } public function testProcessEscapesPhpTags() @@ -151,7 +155,8 @@ public function testProcessEscapesPhpTags() $response = new Response(''); $this->assertSame($response, $esi->process($request, $response)); - $this->assertEquals('php cript language=php>', $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', '', ''], $content); } public function testProcessWhenNoSrcInAnEsi() diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php index a1f1f1593d3f3..97cc8fccd03d0 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SsiTest.php @@ -101,13 +101,15 @@ public function testProcess() $response = new Response('foo '); $ssi->process($request, $response); - $this->assertEquals('foo surrogate->handle($this, \'...\', \'\', false) ?>'."\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "...\n\n\n", ''], $content); $this->assertEquals('SSI', $response->headers->get('x-body-eval')); $response = new Response('foo '); $ssi->process($request, $response); - $this->assertEquals("foo surrogate->handle(\$this, 'foo\\'', '', false) ?>\n", $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', 'foo ', "foo'\n\n\n", ''], $content); } public function testProcessEscapesPhpTags() @@ -118,7 +120,8 @@ public function testProcessEscapesPhpTags() $response = new Response(''); $ssi->process($request, $response); - $this->assertEquals('php cript language=php>', $response->getContent()); + $content = explode(substr($response->getContent(), 0, 24), $response->getContent()); + $this->assertSame(['', '', ''], $content); } public function testProcessWhenNoSrcInAnSsi() From 78eff396219679b660f597b0a5099d2bac169f91 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 4 May 2023 09:21:45 +0200 Subject: [PATCH 12/38] [HttpClient] Fix getting through proxies via CONNECT --- .../HttpClient/Response/AmpResponse.php | 3 +- .../HttpClient/Response/CurlResponse.php | 30 +++++++++---------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/Symfony/Component/HttpClient/Response/AmpResponse.php b/src/Symfony/Component/HttpClient/Response/AmpResponse.php index 6d0ce6e33e01f..03e5daf349b77 100644 --- a/src/Symfony/Component/HttpClient/Response/AmpResponse.php +++ b/src/Symfony/Component/HttpClient/Response/AmpResponse.php @@ -47,7 +47,6 @@ final class AmpResponse implements ResponseInterface, StreamableInterface private $multi; private $options; - private $canceller; private $onProgress; private static $delay; @@ -73,7 +72,7 @@ public function __construct(AmpClientState $multi, Request $request, array $opti $info = &$this->info; $headers = &$this->headers; - $canceller = $this->canceller = new CancellationTokenSource(); + $canceller = new CancellationTokenSource(); $handle = &$this->handle; $info['url'] = (string) $request->getUri(); diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index 7cfad581af4c4..2418203060c82 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -76,17 +76,7 @@ public function __construct(CurlClientState $multi, $ch, array $options = null, } curl_setopt($ch, \CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int { - if (0 !== substr_compare($data, "\r\n", -2)) { - return 0; - } - - $len = 0; - - foreach (explode("\r\n", substr($data, 0, -2)) as $data) { - $len += 2 + self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); - } - - return $len; + return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); }); if (null === $options) { @@ -381,19 +371,29 @@ private static function select(ClientState $multi, float $timeout): int */ private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int { + if (!str_ends_with($data, "\r\n")) { + return 0; + } + $waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0'; if ('H' !== $waitFor[0]) { return \strlen($data); // Ignore HTTP trailers } - if ('' !== $data) { + $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE); + + if ($statusCode !== $info['http_code'] && !preg_match("#^HTTP/\d+(?:\.\d+)? {$statusCode}(?: |\r\n$)#", $data)) { + return \strlen($data); // Ignore headers from responses to CONNECT requests + } + + if ("\r\n" !== $data) { // Regular header line: add it to the list - self::addResponseHeaders([$data], $info, $headers); + self::addResponseHeaders([substr($data, 0, -2)], $info, $headers); if (!str_starts_with($data, 'HTTP/')) { if (0 === stripos($data, 'Location:')) { - $location = trim(substr($data, 9)); + $location = trim(substr($data, 9, -2)); } return \strlen($data); @@ -416,7 +416,7 @@ private static function parseHeaderLine($ch, string $data, array &$info, array & // End of headers: handle informational responses, redirects, etc. - if (200 > $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE)) { + if (200 > $statusCode) { $multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers); $location = null; From fe5255bb6d13d1ed00cdb8768780db9c74669e1d Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 5 May 2023 12:56:46 +0200 Subject: [PATCH 13/38] Remove usage of constant for better consistency across the codebase --- src/Symfony/Component/Console/Command/CompleteCommand.php | 4 ++-- .../Component/Console/Command/DumpCompletionCommand.php | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Console/Command/CompleteCommand.php b/src/Symfony/Component/Console/Command/CompleteCommand.php index 11ada4e4489b3..0e35143c3335d 100644 --- a/src/Symfony/Component/Console/Command/CompleteCommand.php +++ b/src/Symfony/Component/Console/Command/CompleteCommand.php @@ -155,10 +155,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw $e; } - return self::FAILURE; + return 2; } - return self::SUCCESS; + return 0; } private function createCompletionInput(InputInterface $input): CompletionInput diff --git a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php index 6f809e2f139a1..eaf22be1a9ad4 100644 --- a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php +++ b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php @@ -85,7 +85,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->getOption('debug')) { $this->tailDebugLog($commandName, $output); - return self::SUCCESS; + return 0; } $shell = $input->getArgument('shell') ?? self::guessShell(); @@ -102,12 +102,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln(sprintf('Shell not detected, Symfony shell completion only supports "%s").', implode('", "', $supportedShells))); } - return self::INVALID; + return 2; } $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, $this->getApplication()->getVersion()], file_get_contents($completionFile))); - return self::SUCCESS; + return 0; } private static function guessShell(): string From 3c49177c306bdec8e01320b7cae61330be20f462 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 5 May 2023 15:20:02 +0200 Subject: [PATCH 14/38] [HttpKernel] Fix restoring surrogate content from cache --- .../HttpCache/AbstractSurrogate.php | 11 +++++ .../Component/HttpKernel/HttpCache/Esi.php | 4 +- .../HttpKernel/HttpCache/HttpCache.php | 30 +++++++------- .../Component/HttpKernel/HttpCache/Ssi.php | 5 +-- .../Component/HttpKernel/HttpCache/Store.php | 14 ++++++- .../Tests/HttpCache/HttpCacheTestCase.php | 2 +- .../HttpKernel/Tests/HttpCache/StoreTest.php | 41 +++++++++++++++---- 7 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php index f2d809e8de97d..e1d73dc74827d 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/AbstractSurrogate.php @@ -133,4 +133,15 @@ protected function removeFromControl(Response $response) $response->headers->set('Surrogate-Control', preg_replace(sprintf('#content="%s/1.0",\s*#', $upperName), '', $value)); } } + + protected static function generateBodyEvalBoundary(): string + { + static $cookie; + $cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true); + $boundary = base64_encode($cookie); + + \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === \strlen($boundary)); + + return $boundary; + } } diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php index 4d86508a42092..9f453249325b2 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Esi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Esi.php @@ -80,9 +80,7 @@ public function process(Request $request, Response $response) $content = preg_replace('#.*?#s', '', $content); $content = preg_replace('#]+>#s', '', $content); - static $cookie; - $cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true); - $boundary = base64_encode($cookie); + $boundary = self::generateBodyEvalBoundary(); $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); $i = 1; diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 063d4105b160b..b01bd722607a9 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -29,6 +29,8 @@ */ class HttpCache implements HttpKernelInterface, TerminableInterface { + public const BODY_EVAL_BOUNDARY_LENGTH = 24; + private $kernel; private $store; private $request; @@ -631,26 +633,22 @@ protected function store(Request $request, Response $response) private function restoreResponseBody(Request $request, Response $response) { if ($response->headers->has('X-Body-Eval')) { - ob_start(); + \assert(self::BODY_EVAL_BOUNDARY_LENGTH === 24); - if ($response->headers->has('X-Body-File')) { - include $response->headers->get('X-Body-File'); - } else { - $content = $response->getContent(); + ob_start(); - if (substr($content, -24) === $boundary = substr($content, 0, 24)) { - $j = strpos($content, $boundary, 24); - echo substr($content, 24, $j - 24); - $i = $j + 24; + $content = $response->getContent(); + $boundary = substr($content, 0, 24); + $j = strpos($content, $boundary, 24); + echo substr($content, 24, $j - 24); + $i = $j + 24; - while (false !== $j = strpos($content, $boundary, $i)) { - [$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4); - $i = $j + 24; + while (false !== $j = strpos($content, $boundary, $i)) { + [$uri, $alt, $ignoreErrors, $part] = explode("\n", substr($content, $i, $j - $i), 4); + $i = $j + 24; - echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors); - echo $part; - } - } + echo $this->surrogate->handle($this, $uri, $alt, $ignoreErrors); + echo $part; } $response->setContent(ob_get_clean()); diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php index bb48238ff1f4b..61909100e6157 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Ssi.php @@ -64,10 +64,7 @@ public function process(Request $request, Response $response) // we don't use a proper XML parser here as we can have SSI tags in a plain text response $content = $response->getContent(); - - static $cookie; - $cookie = hash('md5', $cookie ?? $cookie = random_bytes(16), true); - $boundary = base64_encode($cookie); + $boundary = self::generateBodyEvalBoundary(); $chunks = preg_split('##', $content, -1, \PREG_SPLIT_DELIM_CAPTURE); $i = 1; diff --git a/src/Symfony/Component/HttpKernel/HttpCache/Store.php b/src/Symfony/Component/HttpKernel/HttpCache/Store.php index 5db94f73d68c2..9d7f3e4f6949d 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/Store.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/Store.php @@ -475,15 +475,25 @@ private function persistResponse(Response $response): array /** * Restores a Response from the HTTP headers and body. */ - private function restoreResponse(array $headers, string $path = null): Response + private function restoreResponse(array $headers, string $path = null): ?Response { $status = $headers['X-Status'][0]; unset($headers['X-Status']); + $content = null; if (null !== $path) { $headers['X-Body-File'] = [$path]; + unset($headers['x-body-file']); + + if ($headers['X-Body-Eval'] ?? $headers['x-body-eval'] ?? false) { + $content = file_get_contents($path); + \assert(HttpCache::BODY_EVAL_BOUNDARY_LENGTH === 24); + if (48 > \strlen($content) || substr($content, -24) !== substr($content, 0, 24)) { + return null; + } + } } - return new Response($path, $status, $headers); + return new Response($content, $status, $headers); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php index e47631d1780ea..c8b48ff811c76 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php @@ -18,7 +18,7 @@ use Symfony\Component\HttpKernel\HttpCache\Store; use Symfony\Component\HttpKernel\HttpKernelInterface; -class HttpCacheTestCase extends TestCase +abstract class HttpCacheTestCase extends TestCase { protected $kernel; protected $cache; diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php index 239361bc8c337..aff5329cc96f8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php @@ -200,7 +200,7 @@ public function testRestoresResponseContentFromEntityStoreWithLookup() { $this->storeSimpleEntry(); $response = $this->store->lookup($this->request); - $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test')), $response->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test')), $response->headers->get('X-Body-File')); } public function testInvalidatesMetaAndEntityStoreEntriesWithInvalidate() @@ -253,9 +253,9 @@ public function testStoresMultipleResponsesForEachVaryCombination() $res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']); $this->store->write($req3, $res3); - $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent()); - $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent()); - $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File')); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File')); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File')); $this->assertCount(3, $this->getStoreMetadata($key)); } @@ -265,17 +265,17 @@ public function testOverwritesNonVaryingResponseWithStore() $req1 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $res1 = new Response('test 1', 200, ['Vary' => 'Foo Bar']); $this->store->write($req1, $res1); - $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 1')), $this->store->lookup($req1)->headers->get('X-Body-File')); $req2 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam']); $res2 = new Response('test 2', 200, ['Vary' => 'Foo Bar']); $this->store->write($req2, $res2); - $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 2')), $this->store->lookup($req2)->headers->get('X-Body-File')); $req3 = Request::create('/test', 'get', [], [], [], ['HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar']); $res3 = new Response('test 3', 200, ['Vary' => 'Foo Bar']); $key = $this->store->write($req3, $res3); - $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->getContent()); + $this->assertEquals($this->getStorePath('en'.hash('sha256', 'test 3')), $this->store->lookup($req3)->headers->get('X-Body-File')); $this->assertCount(2, $this->getStoreMetadata($key)); } @@ -330,6 +330,33 @@ public function testDoesNotStorePrivateHeaders() $this->assertNotEmpty($response->headers->getCookies()); } + public function testDiscardsInvalidBodyEval() + { + $request = Request::create('https://example.com/foo'); + $response = new Response('foo', 200, ['X-Body-Eval' => 'SSI']); + + $this->store->write($request, $response); + $this->assertNull($this->store->lookup($request)); + + $request = Request::create('https://example.com/foo'); + $content = str_repeat('a', 24).'b'.str_repeat('a', 24).'b'; + $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']); + + $this->store->write($request, $response); + $this->assertNull($this->store->lookup($request)); + } + + public function testLoadsBodyEval() + { + $request = Request::create('https://example.com/foo'); + $content = str_repeat('a', 24).'b'.str_repeat('a', 24); + $response = new Response($content, 200, ['X-Body-Eval' => 'SSI']); + + $this->store->write($request, $response); + $response = $this->store->lookup($request); + $this->assertSame($content, $response->getContent()); + } + protected function storeSimpleEntry($path = null, $headers = []) { if (null === $path) { From 668be942ac17fd15f19ff88126b9a3b2553e7056 Mon Sep 17 00:00:00 2001 From: MatTheCat Date: Fri, 5 May 2023 16:40:17 +0200 Subject: [PATCH 15/38] =?UTF-8?q?Do=20not=20check=20errored=20definitions?= =?UTF-8?q?=E2=80=99=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Compiler/CheckTypeDeclarationsPass.php | 2 +- .../Compiler/CheckTypeDeclarationsPassTest.php | 15 +++++++++++++++ .../BarErroredDependency.php | 10 ++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php index b7ec85cefb489..59e39067e777e 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckTypeDeclarationsPass.php @@ -210,7 +210,7 @@ private function checkType(Definition $checkedDefinition, $value, \ReflectionPar $class = null; if ($value instanceof Definition) { - if ($value->getFactory()) { + if ($value->hasErrors() || $value->getFactory()) { return; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php index 97e167c99cb10..e3c87ef13b74f 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckTypeDeclarationsPassTest.php @@ -23,6 +23,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\Bar; +use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarErroredDependency; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarMethodCall; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgument; use Symfony\Component\DependencyInjection\Tests\Fixtures\CheckTypeDeclarationsPass\BarOptionalArgumentNotNull; @@ -1010,6 +1011,20 @@ public function testIgnoreDefinitionFactoryArgument() $this->addToAssertionCount(1); } + + public function testErroredDefinitionsAreNotChecked() + { + $container = new ContainerBuilder(); + $container->register('errored_dependency', BarErroredDependency::class) + ->setArguments([ + 8000 (new Definition(Foo::class)) + ->addError('error'), + ]); + + (new CheckTypeDeclarationsPass(true))->process($container); + + $this->addToAssertionCount(1); + } } class CallableClass diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php new file mode 100644 index 0000000000000..d1368c3f7ef44 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/BarErroredDependency.php @@ -0,0 +1,10 @@ + Date: Sun, 7 May 2023 14:06:34 +0200 Subject: [PATCH 16/38] [HttpClient] Fix setting duplicate-name headers when redirecting with AmpHttpClient --- src/Symfony/Component/HttpClient/Response/AmpResponse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpClient/Response/AmpResponse.php b/src/Symfony/Component/HttpClient/Response/AmpResponse.php index 6d0ce6e33e01f..3fac5860bb188 100644 --- a/src/Symfony/Component/HttpClient/Response/AmpResponse.php +++ b/src/Symfony/Component/HttpClient/Response/AmpResponse.php @@ -358,7 +358,7 @@ private static function followRedirects(Request $originRequest, AmpClientState $ } foreach ($originRequest->getRawHeaders() as [$name, $value]) { - $request->setHeader($name, $value); + $request->addHeader($name, $value); } if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) { From b0ec0c75d9bba1e151e8003c39b852b4db9711cf Mon Sep 17 00:00:00 2001 From: Laurent VOULLEMIER Date: Mon, 8 May 2023 21:46:50 +0200 Subject: [PATCH 17/38] Fix param type annotation --- src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php index 71da329a996cd..eb835caa41b25 100644 --- a/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php +++ b/src/Symfony/Bridge/Doctrine/Middleware/Debug/Query.php @@ -46,8 +46,8 @@ public function stop(): void } /** - * @param string|int $param - * @param string|int|float|bool|null $variable + * @param string|int $param + * @param mixed $variable */ public function setParam($param, &$variable, int $type): void { From adb9393a9cf81c32e6d42c3adb6d00da3b68dcd7 Mon Sep 17 00:00:00 2001 From: Lane Shukhov Date: Mon, 8 May 2023 20:35:45 +0300 Subject: [PATCH 18/38] [HttpFoundation] Fix file streaming after connection aborted --- src/Symfony/Component/HttpFoundation/BinaryFileResponse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php index 03cf1a23ee3d8..d3caa36aa08ff 100644 --- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php +++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php @@ -349,7 +349,7 @@ public function sendContent() while ('' !== $data) { $read = fwrite($out, $data); if (false === $read || connection_aborted()) { - break; + break 2; } if (0 < $length) { $length -= $read; From a5d1ca6caaf56c900b993ce8d8934b1147a72302 Mon Sep 17 00:00:00 2001 From: Maxim Tyugaev Date: Sat, 6 May 2023 21:20:54 +0300 Subject: [PATCH 19/38] [Serializer] Handle datetime deserialization in U format --- .../Serializer/Normalizer/DateTimeNormalizer.php | 9 ++++++++- .../Tests/Normalizer/DateTimeNormalizerTest.php | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php index 71df2e6bdfb7c..9736d8e78e4a0 100644 --- a/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/DateTimeNormalizer.php @@ -91,7 +91,14 @@ public function denormalize($data, string $type, string $format = null, array $c $dateTimeFormat = $context[self::FORMAT_KEY] ?? null; $timezone = $this->getTimezone($context); - if (null === $data || !\is_string($data) || '' === trim($data)) { + if (\is_int($data) || \is_float($data)) { + switch ($dateTimeFormat) { + case 'U': $data = sprintf('%d', $data); break; + case 'U.u': $data = sprintf('%.6F', $data); break; + } + } + + if (!\is_string($data) || '' === trim($data)) { throw NotNormalizableValueException::createForUnexpectedDataType('The data is either not an string, an empty string, or null; you should pass a string that can be parsed with the passed format or a valid DateTime string.', $data, [Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true); } diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php index 674dfaab5382d..25b7c784fe0e2 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/DateTimeNormalizerTest.php @@ -178,6 +178,8 @@ public function testDenormalize() $this->assertEquals(new \DateTimeImmutable('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTimeImmutable::class)); $this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize('2016-01-01T00:00:00+00:00', \DateTime::class)); $this->assertEquals(new \DateTime('2016/01/01', new \DateTimeZone('UTC')), $this->normalizer->denormalize(' 2016-01-01T00:00:00+00:00 ', \DateTime::class)); + $this->assertEquals(new \DateTimeImmutable('2023-05-06T17:35:34.000000+0000', new \DateTimeZone('UTC')), $this->normalizer->denormalize(1683394534, \DateTimeImmutable::class, null, [DateTimeNormalizer::FORMAT_KEY => 'U'])); + $this->assertEquals(new \DateTimeImmutable('2023-05-06T17:35:34.123400+0000', new \DateTimeZone('UTC')), $this->normalizer->denormalize(1683394534.1234, \DateTimeImmutable::class, null, [DateTimeNormalizer::FORMAT_KEY => 'U.u'])); } public function testDenormalizeUsingTimezonePassedInConstructor() From 29eaa54cd1a61ab6ecde84b4f06cd63903387ce8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sat, 13 May 2023 11:29:41 +0200 Subject: [PATCH 20/38] [HttpKernel] Account for Response::getDate() possibly returning a DateTimeImmutable --- .../Component/HttpKernel/HttpCache/ResponseCacheStrategy.php | 2 +- .../HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php index cf8682257ee8f..5f372c566dd44 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php @@ -147,7 +147,7 @@ public function update(Response $response) if (is_numeric($this->ageDirectives['expires'])) { $date = clone $response->getDate(); - $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds'); + $date = $date->modify('+'.($this->ageDirectives['expires'] + $this->age).' seconds'); $response->setExpires($date); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php index c5c510f85832e..ce9f5ba1a158a 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php @@ -257,7 +257,7 @@ public function testCacheControlMerging(array $expects, array $master, array $su case 'expires': $expires = clone $response->getDate(); - $expires->modify('+'.$value.' seconds'); + $expires = $expires->modify('+'.$value.' seconds'); $response->setExpires($expires); break; From dcbdca60ce79e63bc6e8b75cdfbe974a739b8c97 Mon Sep 17 00:00:00 2001 From: Quentin Devos <4972091+Okhoshi@users.noreply.github.com> Date: Sat, 6 May 2023 22:37:52 +0200 Subject: [PATCH 21/38] [FrameworkBundle] Generate caches consistently on successive run of `cache:clear` command --- .../Command/CacheClearCommand.php | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index b0d5565398d2d..053810757a0d8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -137,14 +137,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($output->isVerbose()) { $io->comment('Warming up optional cache...'); } - $warmer = $kernel->getContainer()->get('cache_warmer'); - // non optional warmers already ran during container compilation - $warmer->enableOnlyOptionalWarmers(); - $preload = (array) $warmer->warmUp($realCacheDir); - - if ($preload && file_exists($preloadFile = $realCacheDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) { - Preloader::append($preloadFile, $preload); - } + $this->warmupOptionals($realCacheDir); } } else { $fs->mkdir($warmupDir); @@ -153,7 +146,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($output->isVerbose()) { $io->comment('Warming up cache...'); } - $this->warmup($warmupDir, $realCacheDir, !$input->getOption('no-optional-warmers')); + $this->warmup($warmupDir, $realBuildDir); + + if (!$input->getOption('no-optional-warmers')) { + if ($output->isVerbose()) { + $io->comment('Warming up optional cache...'); + } + $this->warmupOptionals($realCacheDir); + } } if (!$fs->exists($warmupDir.'/'.$containerDir)) { @@ -227,7 +227,7 @@ private function isNfs(string $dir): bool return false; } - private function warmup(string $warmupDir, string $realBuildDir, bool $enableOptionalWarmers = true) + private function warmup(string $warmupDir, string $realBuildDir): void { // create a temporary kernel $kernel = $this->getApplication()->getKernel(); @@ -236,18 +236,6 @@ private function warmup(string $warmupDir, string $realBuildDir, bool $enableOpt } $kernel->reboot($warmupDir); - // warmup temporary dir - if ($enableOptionalWarmers) { - $warmer = $kernel->getContainer()->get('cache_warmer'); - // non optional warmers already ran during container compilation - $warmer->enableOnlyOptionalWarmers(); - $preload = (array) $warmer->warmUp($warmupDir); - - if ($preload && file_exists($preloadFile = $warmupDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) { - Preloader::append($preloadFile, $preload); - } - } - // fix references to cached files with the real cache directory name $search = [$warmupDir, str_replace('\\', '\\\\', $warmupDir)]; $replace = str_replace('\\', '/', $realBuildDir); @@ -258,4 +246,17 @@ private function warmup(string $warmupDir, string $realBuildDir, bool $enableOpt } } } + + private function warmupOptionals(string $realCacheDir): void + { + $kernel = $this->getApplication()->getKernel(); + $warmer = $kernel->getContainer()->get('cache_warmer'); + // non optional warmers already ran during container compilation + $warmer->enableOnlyOptionalWarmers(); + $preload = (array) $warmer->warmUp($realCacheDir); + + if ($preload && file_exists($preloadFile = $realCacheDir.'/'.$kernel->getContainer()->getParameter('kernel.container_class').'.preload.php')) { + Preloader::append($preloadFile, $preload); + } + } } From a55feaae41ccf8542f3354d2a0de7f514d85d08a Mon Sep 17 00:00:00 2001 From: HypeMC Date: Mon, 15 May 2023 22:11:03 +0200 Subject: [PATCH 22/38] [PropertyInfo] Fix `PhpStanExtractor` when constructor has no docblock --- .../Extractor/PhpStanExtractor.php | 5 ++++- .../Tests/Extractor/PhpStanExtractorTest.php | 9 +++++++++ .../ConstructorDummyWithoutDocBlock.php | 19 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.php diff --git a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php index 52a43d16aee46..a964b5036893b 100644 --- a/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php +++ b/src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php @@ -171,7 +171,10 @@ private function getDocBlockFromConstructor(string $class, string $property): ?P return null; } - $rawDocNode = $reflectionConstructor->getDocComment(); + if (!$rawDocNode = $reflectionConstructor->getDocComment()) { + return null; + } + $tokens = new TokenIterator($this->lexer->tokenize($rawDocNode)); $phpDocNode = $this->phpDocParser->parse($tokens); $tokens->consumeTokenType(Lexer::TOKEN_END); diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php index a517e7ac30469..c607f2abc3761 100644 --- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php +++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; +use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock; use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue; use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy; use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy; @@ -353,6 +354,14 @@ public function testExtractConstructorTypes($property, array $type = null) $this->assertEquals($type, $this->extractor->getTypesFromConstructor('Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy', $property)); } + /** + * @dataProvider constructorTypesProvider + */ + public function testExtractConstructorTypesReturnNullOnEmptyDocBlock($property) + { + $this->assertNull($this->extractor->getTypesFromConstructor(ConstructorDummyWithoutDocBlock::class, $property)); + } + public static function constructorTypesProvider() { return [ diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.php new file mode 100644 index 0000000000000..7e2087d467f7d --- /dev/null +++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ConstructorDummyWithoutDocBlock.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyInfo\Tests\Fixtures; + +class ConstructorDummyWithoutDocBlock +{ + public function __construct(\DateTimeZone $timezone, $date, $dateObject, \DateTimeImmutable $dateTime, $mixed) + { + } +} From 0cc9e3ae1f3c5b2f159f8b4501360ab18982c7dd Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Wed, 17 May 2023 00:33:28 +0200 Subject: [PATCH 23/38] [Notifier] Update AmazonSns url in doc from de to en --- src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md index 3dbcbb1546247..1e8e244f014a9 100644 --- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md +++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md @@ -1,7 +1,7 @@ Amazon Notifier =============== -Provides [Amazon SNS](https://aws.amazon.com/de/sns/) integration for Symfony Notifier. +Provides [Amazon SNS](https://aws.amazon.com/en/sns/) integration for Symfony Notifier. DSN example ----------- From c89132bac4c607faaf871727d299490c15dec909 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Wed, 17 May 2023 13:26:05 +0200 Subject: [PATCH 24/38] [Process] Stop the process correctly even if underlying input stream is not closed: While checking a process to end, on posix system, process component only checks if pipes are still open, this fix ensure that if the process is terminated it correctly return, even if the underlying pipe is not closed. It can be useful when using \STDIN as a input stream as it will always be open --- src/Symfony/Component/Process/Process.php | 2 +- src/Symfony/Component/Process/Tests/ProcessTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index b47ecca17bfe5..9b19475ac5d78 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -428,7 +428,7 @@ public function wait(callable $callback = null) do { $this->checkTimeout(); - $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen()); $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); } while ($running); diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index 790167fcade94..36acf02a7cd6b 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -1538,6 +1538,16 @@ public function testEnvCaseInsensitiveOnWindows() } } + public function testNotTerminableInputPipe() + { + $process = $this->getProcess('echo foo'); + $process->setInput(\STDIN); + $process->start(); + $process->setTimeout(2); + $process->wait(); + $this->assertFalse($process->isRunning()); + } + /** * @param string|array $commandline * @param mixed $input From a0163929c8f252de5b0b2c4257eaf6045f6d4c8d Mon Sep 17 00:00:00 2001 From: Giorgio Premi Date: Fri, 12 May 2023 18:17:02 +0200 Subject: [PATCH 25/38] UrlHelper is now aware of RequestContext changes RequestContext in routing can change at runtime after the UrlHelper has been created, so using a RequestContextAwareInterface instance (i.e. the router) will workaround URL generation issues. --- .../Resources/config/services.php | 2 +- .../Bundle/FrameworkBundle/composer.json | 2 +- .../HttpFoundation/Tests/UrlHelperTest.php | 36 ++++++++++++++++++ .../Component/HttpFoundation/UrlHelper.php | 38 +++++++++++++------ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index a26dfb5adc612..cc2810fc6dbb4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -110,7 +110,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] ->set('url_helper', UrlHelper::class) ->args([ service('request_stack'), - service('router.request_context')->ignoreOnInvalid(), + service('router')->ignoreOnInvalid(), ]) ->alias(UrlHelper::class, 'url_helper') diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 089e2e0827fea..19da8ab3b66d6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -24,7 +24,7 @@ "symfony/deprecation-contracts": "^2.1|^3", "symfony/event-dispatcher": "^5.1|^6.0", "symfony/error-handler": "^4.4.1|^5.0.1|^6.0", - "symfony/http-foundation": "^5.3|^6.0", + "symfony/http-foundation": "^5.4.24|^6.2.11", "symfony/http-kernel": "^5.4|^6.0", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16", diff --git a/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php b/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php index 2057dd7097cc8..080b5ffea3482 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\UrlHelper; use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; class UrlHelperTest extends TestCase { @@ -64,11 +65,46 @@ public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host } $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); + $helper = new UrlHelper(new RequestStack(), $requestContext); $this->assertEquals($expected, $helper->getAbsoluteUrl($path)); } + /** + * @dataProvider getGenerateAbsoluteUrlRequestContextData + */ + public function testGenerateAbsoluteUrlWithRequestContextAwareInterface($path, $baseUrl, $host, $scheme, $httpPort, $httpsPort, $expected) + { + if (!class_exists(RequestContext::class)) { + $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.'); + } + + $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); + $contextAware = new class($requestContext) implements RequestContextAwareInterface { + private $requestContext; + + public function __construct($requestContext) + { + $this->requestContext = $requestContext; + } + + public function setContext(RequestContext $context) + { + $this->requestContext = $context; + } + + public function getContext() + { + return $this->requestContext; + } + }; + + $helper = new UrlHelper(new RequestStack(), $contextAware); + + $this->assertEquals($expected, $helper->getAbsoluteUrl($path)); + } + /** * @dataProvider getGenerateAbsoluteUrlRequestContextData */ diff --git a/src/Symfony/Component/HttpFoundation/UrlHelper.php b/src/Symfony/Component/HttpFoundation/UrlHelper.php index c15f101cdf80b..90659947dba6d 100644 --- a/src/Symfony/Component/HttpFoundation/UrlHelper.php +++ b/src/Symfony/Component/HttpFoundation/UrlHelper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; /** * A helper service for manipulating URLs within and outside the request scope. @@ -23,8 +24,15 @@ final class UrlHelper private $requestStack; private $requestContext; - public function __construct(RequestStack $requestStack, RequestContext $requestContext = null) + /** + * @param RequestContextAwareInterface|RequestContext|null $requestContext + */ + public function __construct(RequestStack $requestStack, $requestContext = null) { + if (null !== $requestContext && !$requestContext instanceof RequestContext && !$requestContext instanceof RequestContextAwareInterface) { + throw new \TypeError(__METHOD__.': Argument #2 ($requestContext) must of type Symfony\Component\Routing\RequestContextAwareInterface|Symfony\Component\Routing\RequestContext|null, '.get_debug_type($requestContext).' given.'); + } + $this->requestStack = $requestStack; $this->requestContext = $requestContext; } @@ -73,28 +81,36 @@ public function getRelativePath(string $path): string private function getAbsoluteUrlFromContext(string $path): string { - if (null === $this->requestContext || '' === $host = $this->requestContext->getHost()) { + if (null === $context = $this->requestContext) { + return $path; + } + + if ($context instanceof RequestContextAwareInterface) { + $context = $context->getContext(); + } + + if ('' === $host = $context->getHost()) { return $path; } - $scheme = $this->requestContext->getScheme(); + $scheme = $context->getScheme(); $port = ''; - if ('http' === $scheme && 80 !== $this->requestContext->getHttpPort()) { - $port = ':'.$this->requestContext->getHttpPort(); - } elseif ('https' === $scheme && 443 !== $this->requestContext->getHttpsPort()) { - $port = ':'.$this->requestContext->getHttpsPort(); + if ('http' === $scheme && 80 !== $context->getHttpPort()) { + $port = ':'.$context->getHttpPort(); + } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) { + $port = ':'.$context->getHttpsPort(); } if ('#' === $path[0]) { - $queryString = $this->requestContext->getQueryString(); - $path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path; + $queryString = $context->getQueryString(); + $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path; } elseif ('?' === $path[0]) { - $path = $this->requestContext->getPathInfo().$path; + $path = $context->getPathInfo().$path; } if ('/' !== $path[0]) { - $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path; + $path = rtrim($context->getBaseUrl(), '/').'/'.$path; } return $scheme.'://'.$host.$port.$path; From 75eb46949d9fae3703fcce9907e29d72357fc0f2 Mon Sep 17 00:00:00 2001 From: Giorgio Premi Date: Fri, 12 May 2023 18:17:02 +0200 Subject: [PATCH 26/38] [HttpFoundation] UrlHelper is now aware of RequestContext changes --- .../Resources/config/services.php | 2 +- .../Bundle/FrameworkBundle/composer.json | 2 +- .../HttpFoundation/Tests/UrlHelperTest.php | 35 +++++++++++++++++ .../Component/HttpFoundation/UrlHelper.php | 38 +++++++++++++------ 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index a26dfb5adc612..cc2810fc6dbb4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -110,7 +110,7 @@ class_exists(WorkflowEvents::class) ? WorkflowEvents::ALIASES : [] ->set('url_helper', UrlHelper::class) ->args([ service('request_stack'), - service('router.request_context')->ignoreOnInvalid(), + service('router')->ignoreOnInvalid(), ]) ->alias(UrlHelper::class, 'url_helper') diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 089e2e0827fea..19da8ab3b66d6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -24,7 +24,7 @@ "symfony/deprecation-contracts": "^2.1|^3", "symfony/event-dispatcher": "^5.1|^6.0", "symfony/error-handler": "^4.4.1|^5.0.1|^6.0", - "symfony/http-foundation": "^5.3|^6.0", + "symfony/http-foundation": "^5.4.24|^6.2.11", "symfony/http-kernel": "^5.4|^6.0", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16", diff --git a/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php b/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php index 2057dd7097cc8..25640402af238 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/UrlHelperTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\UrlHelper; use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; class UrlHelperTest extends TestCase { @@ -69,6 +70,40 @@ public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host $this->assertEquals($expected, $helper->getAbsoluteUrl($path)); } + /** + * @dataProvider getGenerateAbsoluteUrlRequestContextData + */ + public function testGenerateAbsoluteUrlWithRequestContextAwareInterface($path, $baseUrl, $host, $scheme, $httpPort, $httpsPort, $expected) + { + if (!class_exists(RequestContext::class)) { + $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.'); + } + + $requestContext = new RequestContext($baseUrl, 'GET', $host, $scheme, $httpPort, $httpsPort, $path); + $contextAware = new class($requestContext) implements RequestContextAwareInterface { + private $requestContext; + + public function __construct($requestContext) + { + $this->requestContext = $requestContext; + } + + public function setContext(RequestContext $context) + { + $this->requestContext = $context; + } + + public function getContext() + { + return $this->requestContext; + } + }; + + $helper = new UrlHelper(new RequestStack(), $contextAware); + + $this->assertEquals($expected, $helper->getAbsoluteUrl($path)); + } + /** * @dataProvider getGenerateAbsoluteUrlRequestContextData */ diff --git a/src/Symfony/Component/HttpFoundation/UrlHelper.php b/src/Symfony/Component/HttpFoundation/UrlHelper.php index c15f101cdf80b..90659947dba6d 100644 --- a/src/Symfony/Component/HttpFoundation/UrlHelper.php +++ b/src/Symfony/Component/HttpFoundation/UrlHelper.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpFoundation; use Symfony\Component\Routing\RequestContext 8000 ; +use Symfony\Component\Routing\RequestContextAwareInterface; /** * A helper service for manipulating URLs within and outside the request scope. @@ -23,8 +24,15 @@ final class UrlHelper private $requestStack; private $requestContext; - public function __construct(RequestStack $requestStack, RequestContext $requestContext = null) + /** + * @param RequestContextAwareInterface|RequestContext|null $requestContext + */ + public function __construct(RequestStack $requestStack, $requestContext = null) { + if (null !== $requestContext && !$requestContext instanceof RequestContext && !$requestContext instanceof RequestContextAwareInterface) { + throw new \TypeError(__METHOD__.': Argument #2 ($requestContext) must of type Symfony\Component\Routing\RequestContextAwareInterface|Symfony\Component\Routing\RequestContext|null, '.get_debug_type($requestContext).' given.'); + } + $this->requestStack = $requestStack; $this->requestContext = $requestContext; } @@ -73,28 +81,36 @@ public function getRelativePath(string $path): string private function getAbsoluteUrlFromContext(string $path): string { - if (null === $this->requestContext || '' === $host = $this->requestContext->getHost()) { + if (null === $context = $this->requestContext) { + return $path; + } + + if ($context instanceof RequestContextAwareInterface) { + $context = $context->getContext(); + } + + if ('' === $host = $context->getHost()) { return $path; } - $scheme = $this->requestContext->getScheme(); + $scheme = $context->getScheme(); $port = ''; - if ('http' === $scheme && 80 !== $this->requestContext->getHttpPort()) { - $port = ':'.$this->requestContext->getHttpPort(); - } elseif ('https' === $scheme && 443 !== $this->requestContext->getHttpsPort()) { - $port = ':'.$this->requestContext->getHttpsPort(); + if ('http' === $scheme && 80 !== $context->getHttpPort()) { + $port = ':'.$context->getHttpPort(); + } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) { + $port = ':'.$context->getHttpsPort(); } if ('#' === $path[0]) { - $queryString = $this->requestContext->getQueryString(); - $path = $this->requestContext->getPathInfo().($queryString ? '?'.$queryString : '').$path; + $queryString = $context->getQueryString(); + $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path; } elseif ('?' === $path[0]) { - $path = $this->requestContext->getPathInfo().$path; + $path = $context->getPathInfo().$path; } if ('/' !== $path[0]) { - $path = rtrim($this->requestContext->getBaseUrl(), '/').'/'.$path; + $path = rtrim($context->getBaseUrl(), '/').'/'.$path; } return $scheme.'://'.$host.$port.$path; From 9cc5cbfd585ed922992aba514c7f1f505593cb59 Mon Sep 17 00:00:00 2001 From: Andrew Forster Date: Tue, 16 May 2023 22:19:41 -0500 Subject: [PATCH 27/38] [FrameworkBundle] Fix Workflow without a marking store definition uses marking store definition of previously defined workflow --- .../FrameworkExtension.php | 3 +- .../PhpFrameworkExtensionTest.php | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 10d479a11c699..82b798b18e698 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -920,6 +920,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $workflows[$workflowId] = $definitionDefinition; // Create MarkingStore + $markingStoreDefinition = null; if (isset($workflow['marking_store']['type'])) { $markingStoreDefinition = new ChildDefinition('workflow.marking_store.method'); $markingStoreDefinition->setArguments([ @@ -933,7 +934,7 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ // Create Workflow $workflowDefinition = new ChildDefinition(sprintf('%s.abstract', $type)); $workflowDefinition->replaceArgument(0, new Reference(sprintf('%s.definition', $workflowId))); - $workflowDefinition->replaceArgument(1, $markingStoreDefinition ?? null); + $workflowDefinition->replaceArgument(1, $markingStoreDefinition); $workflowDefinition->replaceArgument(3, $name); $workflowDefinition->replaceArgument(4, $workflow['events_to_dispatch']); $workflowDefinition->addTag('container.private', [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index c763e2bd211b4..871b62e8811da 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -85,6 +85,62 @@ public function testWorkflowValidationStateMachine() }); } + public function testWorkflowDefaultMarkingStoreDefinition() + { + $container = $this->createContainerFromClosure(function ($container) { + $container->loadFromExtension('framework', [ + 'workflows' => [ + 'workflow_a' => [ + 'type' => 'state_machine', + 'marking_store' => [ + 'type' => 'method', + 'property' => 'status', + ], + 'supports' => [ + __CLASS__, + ], + 'places' => [ + 'a', + 'b', + ], + 'transitions' => [ + 'a_to_b' => [ + 'from' => ['a'], + 'to' => ['b'], + ], + ], + ], + 'workflow_b' => [ + 'type' => 'state_machine', + 'supports' => [ + __CLASS__, + ], + 'places' => [ + 'a', + 'b', + ], + 'transitions' => [ + 'a_to_b' => [ + 'from' => ['a'], + 'to' => ['b'], + ], + ], + ], + ], + ]); + }); + + $workflowA = $container->getDefinition('state_machine.workflow_a'); + $argumentsA = $workflowA->getArguments(); + $this->assertArrayHasKey('index_1', $argumentsA, 'workflow_a has a marking_store argument'); + $this->assertNotNull($argumentsA['index_1'], 'workflow_a marking_store argument is not null'); + + $workflowB = $container->getDefinition('state_machine.workflow_b'); + $argumentsB = $workflowB->getArguments(); + $this->assertArrayHasKey('index_1', $argumentsB, 'workflow_b has a marking_store argument'); + $this->assertNull($argumentsB['index_1'], 'workflow_b marking_store argument is null'); + } + public function testRateLimiterWithLockFactory() { try { From bf1ae1ad7be2798ce3858d45a0119d79db1196a7 Mon Sep 17 00:00:00 2001 From: Maximilian Beckers Date: Fri, 19 May 2023 11:40:46 +0200 Subject: [PATCH 28/38] [Console] Remove exec and replace it by shell_exec --- src/Symfony/Component/Console/Helper/QuestionHelper.php | 6 ++---- src/Symfony/Component/Console/Terminal.php | 8 +++----- .../Component/Console/Tests/Helper/QuestionHelperTest.php | 4 ++-- src/Symfony/Component/Console/Tests/TerminalTest.php | 4 ++-- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 10602038c299f..ec5b418f2cbe7 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -500,13 +500,11 @@ private function isInteractiveInput($inputStream): bool return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r')); } - if (!\function_exists('exec')) { + if (!\function_exists('shell_exec')) { return self::$stdinIsInteractive = true; } - exec('stty 2> /dev/null', $output, $status); - - return self::$stdinIsInteractive = 1 !== $status; + return self::$stdinIsInteractive = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); } /** diff --git a/src/Symfony/Component/Console/Terminal.php b/src/Symfony/Component/Console/Terminal.php index 04653d3bd2bbc..b91e8afc5cac4 100644 --- a/src/Symfony/Component/Console/Terminal.php +++ b/src/Symfony/Component/Console/Terminal.php @@ -64,14 +64,12 @@ public static function hasSttyAvailable(): bool return self::$stty; } - // skip check if exec function is disabled - if (!\function_exists('exec')) { + // skip check if shell_exec function is disabled + if (!\function_exists('shell_exec')) { return false; } - exec('stty 2>&1', $output, $exitcode); - - return self::$stty = 0 === $exitcode; + return self::$stty = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); } private static function initDimensions() diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php index 9761e8f979345..74315d8982638 100644 --- a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php @@ -430,7 +430,7 @@ public function testAskHiddenResponse() $this->assertEquals('8AM', $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream("8AM\n")), $this->createOutputInterface(), $question)); } - public function testAskHiddenResponseTrimmed() + public function testAskHiddenResponseNotTrimmed() { if ('\\' === \DIRECTORY_SEPARATOR) { $this->markTestSkipped('This test is not supported on Windows'); @@ -442,7 +442,7 @@ public function testAskHiddenResponseTrimmed() $question->setHidden(true); $question->setTrimmable(false); - $this->assertEquals(' 8AM', $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream(' 8AM')), $this->createOutputInterface(), $question)); + $this->assertEquals(' 8AM'.\PHP_EOL, $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream(' 8AM'.\PHP_EOL)), $this->createOutputInterface(), $question)); } public function testAskMultilineResponseWithEOF() diff --git a/src/Symfony/Component/Console/Tests/TerminalTest.php b/src/Symfony/Component/Console/Tests/TerminalTest.php index c2d36fe0ab899..34d32b08ebecb 100644 --- a/src/Symfony/Component/Console/Tests/TerminalTest.php +++ b/src/Symfony/Component/Console/Tests/TerminalTest.php @@ -77,8 +77,8 @@ public function testSttyOnWindows() $this->markTestSkipped('Must be on windows'); } - $sttyString = exec('(stty -a | grep columns) 2>&1', $output, $exitcode); - if (0 !== $exitcode) { + $sttyString = shell_exec('(stty -a | grep columns) 2> NUL'); + if (!$sttyString) { $this->markTestSkipped('Must have stty support'); } From 4be2036b61e57a86b2776bcad4ba23d2a0052f7e Mon Sep 17 00:00:00 2001 From: Robert Korulczyk Date: Sat, 13 May 2023 13:20:31 +0200 Subject: [PATCH 29/38] [Translation] Fix handling of null messages in `ArrayLoader` --- src/Symfony/Component/Translation/Loader/ArrayLoader.php | 6 ++++-- .../Translation/Tests/Loader/YamlFileLoaderTest.php | 9 +++++++++ .../Component/Translation/Tests/fixtures/non-string.yml | 4 ++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/Translation/Tests/fixtures/non-string.yml diff --git a/src/Symfony/Component/Translation/Loader/ArrayLoader.php b/src/Symfony/Component/Translation/Loader/ArrayLoader.php index 0758da8b59a54..feed0de4b0025 100644 --- a/src/Symfony/Component/Translation/Loader/ArrayLoader.php +++ b/src/Symfony/Component/Translation/Loader/ArrayLoader.php @@ -46,9 +46,11 @@ private function flatten(array $messages): array foreach ($messages as $key => $value) { if (\is_array($value)) { foreach ($this->flatten($value) as $k => $v) { - $result[$key.'.'.$k] = $v; + if (null !== $v) { + $result[$key.'.'.$k] = $v; + } } - } else { + } elseif (null !== $value) { $result[$key] = $value; } } diff --git a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php index 230c02e539e45..cac0b6f87823a 100644 --- a/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/Translation/Tests/Loader/YamlFileLoaderTest.php @@ -30,6 +30,15 @@ public function testLoad() $this->assertEquals([new FileResource($resource)], $catalogue->getResources()); } + public function testLoadNonStringMessages() + { + $loader = new YamlFileLoader(); + $resource = __DIR__.'/../fixtures/non-string.yml'; + $catalogue = $loader->load($resource, 'en', 'domain1'); + + $this->assertSame(['root.foo2' => '', 'root.bar' => 'bar'], $catalogue->all('domain1')); + } + public function testLoadDoesNothingIfEmpty() { $loader = new YamlFileLoader(); diff --git a/src/Symfony/Component/Translation/Tests/fixtures/non-string.yml b/src/Symfony/Component/Translation/Tests/fixtures/non-string.yml new file mode 100644 index 0000000000000..41e245e19a4a6 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/fixtures/non-string.yml @@ -0,0 +1,4 @@ +root: + foo1: + foo2: '' + bar: 'bar' From b670273f2fa8c4a6d4557e1a0c9222f4ca45d070 Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Sat, 20 May 2023 23:36:46 +0200 Subject: [PATCH 30/38] [Notifier] Document Notifier options in README files 5.4 --- .../Bridge/AmazonSns/AmazonSnsOptions.php | 4 ++-- .../Notifier/Bridge/AmazonSns/README.md | 24 +++++++++++++++++++ .../Component/Notifier/Bridge/Mobyt/README.md | 23 ++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php b/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php index fe82d23972325..9e482c156e78b 100644 --- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php +++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/AmazonSnsOptions.php @@ -52,7 +52,7 @@ public function recipient(string $topic): self } /** - * @see PublishInput::$Subject + * @see PublishInput::$subject * * @return $this */ @@ -64,7 +64,7 @@ public function subject(string $subject): self } /** - * @see PublishInput::$MessageStructure + * @see PublishInput::$messageStructure * * @return $this */ diff --git a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md index 1e8e244f014a9..db4759327f502 100644 --- a/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md +++ b/src/Symfony/Component/Notifier/Bridge/AmazonSns/README.md @@ -10,6 +10,30 @@ DSN example AMAZON_SNS_DSN=sns://ACCESS_ID:ACCESS_KEY@default?region=REGION ``` +Adding Options to a Chat Message +-------------------------------- + +With an Amazon SNS Chat Message, you can use the `AmazonSnsOptions` class to add +message options. + +```php +use Symfony\Component\Notifier\Message\ChatMessage; +use Symfony\Component\Notifier\Bridge\AmazonSns\AmazonSnsOptions; + +$chatMessage = new ChatMessage('Contribute To Symfony'); + +$options = (new AmazonSnsOptions('topic_arn')) + ->subject('subject') + ->messageStructure('json') + // ... + ; + +// Add the custom options to the chat message and send the message +$chatMessage->options($options); + +$chatter->send($chatMessage); +``` + Resources --------- diff --git a/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md b/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md index 2d9b0b6d1488d..d9760759c9f90 100644 --- a/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md +++ b/src/Symfony/Component/Notifier/Bridge/Mobyt/README.md @@ -16,6 +16,29 @@ where: - `FROM` is the sender - `TYPE_QUALITY` is the quality of your message: `N` for high, `L` for medium, `LL` for low (default: `L`) +Adding Options to a Message +--------------------------- + +With a Mobyt Message, you can use the `MobytOptions` class to add +[message options](https://gatewayapi.com/docs/apis/rest/). + +```php +use Symfony\Component\Notifier\Message\SmsMessage; +use Symfony\Component\Notifier\Bridge\Mobyt\MobytOptions; + +$sms = new SmsMessage('+1411111111', 'My message'); + +$options = (new MobytOptions()) + ->messageType(MobytOptions::MESSAGE_TYPE_QUALITY_HIGH) + // ... + ; + +// Add the custom options to the sms message and send the message +$sms->options($options); + +$texter->send($sms); +``` + Resources --------- From 3f0cc24784b4c9a653ea605c1e76419962a553e7 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 23 May 2023 16:09:52 +0200 Subject: [PATCH 31/38] Change default branch in PR template --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 72ea7cfa3e9c0..f7b87a1ce8cd4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | 6.3 for features / 5.4 or 6.2 for bug fixes +| Branch? | 6.4 for features / 5.4, 6.2, or 6.3 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | Deprecations? | yes/no From 36064995e7d032ee2280e53d2e3ae50ebd245789 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 May 2023 18:36:03 +0200 Subject: [PATCH 32/38] Fix CI for experimental mode --- .github/workflows/unit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 69192939d066a..3757f2523e928 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -65,7 +65,7 @@ jobs: echo COLUMNS=120 >> $GITHUB_ENV echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data,integration" >> $GITHUB_ENV - echo COMPOSER_UP='composer update --no-progress --ansi'$([[ "${{ matrix.php }}" = "8.2" ]] && echo ' --ignore-platform-req=php+') >> $GITHUB_ENV + echo COMPOSER_UP='composer update --no-progress --ansi'$([[ "${{ matrix.mode }}" = experimental ]] && echo ' --ignore-platform-req=php+') >> $GITHUB_ENV SYMFONY_VERSIONS=$(git ls-remote -q --heads | cut -f2 | grep -o '/[1-9][0-9]*\.[0-9].*' | sort -V) SYMFONY_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | cut -d "'" -f2 | cut -d '.' -f 1-2) From f09d08c34c72ac3d1469078653716a69e738da3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Wed, 24 May 2023 17:11:52 +0200 Subject: [PATCH 33/38] [Console] Fix PHP Doc of InputArgument --- src/Symfony/Component/Console/Input/InputArgument.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Console/Input/InputArgument.php b/src/Symfony/Component/Console/Input/InputArgument.php index ecfcdad58b907..8a64f7ac8a8e9 100644 --- a/src/Symfony/Component/Console/Input/InputArgument.php +++ b/src/Symfony/Component/Console/Input/InputArgument.php @@ -32,7 +32,7 @@ class InputArgument /** * @param string $name The argument name - * @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL + * @param int|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY * @param string $description A description text * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only) * From 8ae36f3cdc57cce28c9cbef4e0541830cc79a2dc Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Thu, 25 May 2023 14:34:50 +0200 Subject: [PATCH 34/38] [Console] block input stream if needed When the input stream used in the question helper is not blocking, the default value is always used as the stream return false. In order to fix that, we force the stream to be in blocking state and go back to the old state after so other logic is not impacted by this change --- .../Component/Console/Helper/QuestionHelper.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index ec5b418f2cbe7..e236be92a3913 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -128,7 +128,18 @@ private function doAsk(OutputInterface $output, Question $question) } if (false === $ret) { + $isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? true; + + if (!$isBlocked) { + stream_set_blocking($inputStream, true); + } + $ret = $this->readInput($inputStream, $question); + + if (!$isBlocked) { + stream_set_blocking($inputStream, false); + } + if (false === $ret) { throw new MissingInputException('Aborted.'); } From 044bd02a47160ef1a65842d4bc5d07eb5d093422 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 25 May 2023 15:05:00 +0200 Subject: [PATCH 35/38] [5.4] Allow PhpUnitBridge v7 --- composer.json | 4 ++-- src/Symfony/Bridge/Doctrine/composer.json | 1 - src/Symfony/Bundle/FrameworkBundle/composer.json | 1 - src/Symfony/Component/Form/composer.json | 1 - src/Symfony/Component/Validator/composer.json | 1 - src/Symfony/Component/VarDumper/composer.json | 1 - 6 files changed, 2 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index b291459895cb7..469c30715f775 100644 --- a/composer.json +++ b/composer.json @@ -143,7 +143,7 @@ "psr/simple-cache": "^1.0|^2.0", "egulias/email-validator": "^2.1.10|^3.1|^4", "symfony/mercure-bundle": "^0.3", - "symfony/phpunit-bridge": "^5.2|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", "symfony/runtime": "self.version", "symfony/security-acl": "~2.8|~3.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", @@ -161,7 +161,7 @@ "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", "ocramius/proxy-manager": "<2.1", - "phpunit/phpunit": "<5.4.3" + "phpunit/phpunit": "<7.5|9.1.2" }, "config": { "allow-plugins": { diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index a0dbbe9636adb..3aa2e352401ca 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -54,7 +54,6 @@ "doctrine/dbal": "<2.13.1", "doctrine/lexer": "<1.1", "doctrine/orm": "<2.7.4", - "phpunit/phpunit": "<5.4.3", "symfony/cache": "<5.4", "symfony/dependency-injection": "<4.4", "symfony/form": "<5.4.21|>=6,<6.2.7", diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 19da8ab3b66d6..320b993b1f8d1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -74,7 +74,6 @@ "doctrine/persistence": "<1.3", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "phpunit/phpunit": "<5.4.3", "symfony/asset": "<5.3", "symfony/console": "<5.2.5", "symfony/dotenv": "<5.1", diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 6b02d77e29dff..39babd350174e 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -44,7 +44,6 @@ "symfony/uid": "^5.1|^6.0" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<4.4", "symfony/dependency-injection": "<4.4", "symfony/doctrine-bridge": "<5.4.21|>=6,<6.2.7", diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 019a46fc15282..3e860daa30e30 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -49,7 +49,6 @@ "doctrine/annotations": "<1.13", "doctrine/cache": "<1.11", "doctrine/lexer": "<1.1", - "phpunit/phpunit": "<5.4.3", "symfony/dependency-injection": "<4.4", "symfony/expression-language": "<5.1", "symfony/http-kernel": "<4.4", diff --git a/src/Symfony/Component/VarDumper/composer.json b/src/Symfony/Component/VarDumper/composer.json index dc46f58d99eca..fc127d721ab16 100644 --- a/src/Symfony/Component/VarDumper/composer.json +++ b/src/Symfony/Component/VarDumper/composer.json @@ -28,7 +28,6 @@ "twig/twig": "^2.13|^3.0.4" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<4.4" }, "suggest": { From 4a5a327b1ff760ae7d19bef23798e7ab1d543dad Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 27 May 2023 10:05:50 +0200 Subject: [PATCH 36/38] Update CHANGELOG for 5.4.24 --- CHANGELOG-5.4.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG-5.4.md b/CHANGELOG-5.4.md index 8241220390c11..dca25ff5b0d50 100644 --- a/CHANGELOG-5.4.md +++ b/CHANGELOG-5.4.md @@ -7,6 +7,34 @@ in 5.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/v5.4.0...v5.4.1 +* 5.4.24 (2023-05-27) + + * bug #50429 [Console] block input stream if needed (joelwurtz) + * bug #50315 [Translation] Fix handling of null messages in `ArrayLoader` (rob006) + * bug #50338 [Console] Remove ``exec`` and replace it by ``shell_exec`` (maxbeckers) + * bug #50362 [FrameworkBundle] Fix Workflow without a marking store definition uses marking store definition of previously defined workflow (krciga22) + * bug #50309 [HttpFoundation] UrlHelper is now aware of RequestContext changes (giosh94mhz) + * bug #50309 [HttpFoundation] UrlHelper is now aware of RequestContext changes (giosh94mhz) + * bug #50354 [Process] Stop the process correctly even if underlying input stream is not closed (joelwurtz) + * bug #50332 [PropertyInfo] Fix `PhpStanExtractor` when constructor has no docblock (HypeMC) + * bug #50253 [FrameworkBundle] Generate caches consistently on successive run of `cache:clear` command (Okhoshi) + * bug #49063 [Messenger] Respect `isRetryable` decision of the retry strategy for re-delivery (FlyingDR) + * bug #50251 [Serializer] Handle datetime deserialization in U format (tugmaks) + * bug #50266 [HttpFoundation] Fix file streaming after connection aborted (rlshukhov) + * bug #50269 Fix param type annotation (l-vo) + * bug #50256 [HttpClient] Fix setting duplicate-name headers when redirecting with AmpHttpClient (nicolas-grekas) + * bug #50214 [WebProfilerBundle] Remove legacy filters remnants (MatTheCat) + * bug #50235 [HttpClient] Fix getting through proxies via CONNECT (nicolas-grekas) + * bug #50244 [HttpKernel] Fix restoring surrogate content from cache (nicolas-grekas) + * bug #50246 [DependencyInjection] Do not check errored definitions’ type (MatTheCat) + * bug #49557 [PropertyInfo] Fix phpDocExtractor nullable array value type (fabpot) + * bug #50213 [ErrorHandler] Prevent conflicts with WebProfilerBundle’s JavaScript (MatTheCat) + * bug #50192 [Serializer] backed enum throw notNormalizableValueException outside construct method (alli83) + * bug #50238 [HttpKernel] Don't use eval() to render ESI/SSI (nicolas-grekas) + * bug #50226 [HttpClient] Ensure HttplugClient ignores invalid HTTP headers (nicolas-grekas) + * bug #50203 [Messenger] Fix registering message handlers (nicolas-grekas) + * bug #50204 [ErrorHandler] Skip Httplug deprecations for HttplugClient (nicolas-grekas) + * 5.4.23 (2023-04-28) * bug #50143 [Console] trim(): Argument #1 () must be of type string, bool given (danepowell) From 5ab98605fdb93dbd42c83dbbe986ee0f25f9c7ed Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 27 May 2023 10:06:30 +0200 Subject: [PATCH 37/38] Update VERSION for 5.4.24 --- src/Symfony/Component/HttpKernel/Kernel.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 1173f499d023f..f44eac93104ea 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -78,12 +78,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl */ private static $freshCache = []; - public const VERSION = '5.4.24-DEV'; + public const VERSION = '5.4.24'; public const VERSION_ID = 50424; public const MAJOR_VERSION = 5; public const MINOR_VERSION = 4; public const RELEASE_VERSION = 24; - public const EXTRA_VERSION = 'DEV'; + public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '11/2024'; public const END_OF_LIFE = '11/2025'; From b6b2dec5f2351085939fecfe263a30fa3e6da9e1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 27 May 2023 10:08:53 +0200 Subject: [PATCH 38/38] Update CONTRIBUTORS for 5.4.24 --- CONTRIBUTORS.md | 65 +++++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 24afd64907140..cf5f960f7a101 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -21,9 +21,9 @@ The Symfony Connect username in parenthesis allows to get more information - Jordi Boggiano (seldaek) - Roland Franssen (ro0) - Victor Berchet (victor) + - Javier Eguiluz (javier.eguiluz) - Yonel Ceruto (yonelceruto) - Tobias Nyholm (tobias) - - Javier Eguiluz (javier.eguiluz) - Oskar Stark (oskarstark) - Ryan Weaver (weaverryan) - Johannes S (johannes) @@ -40,8 +40,8 @@ The Symfony Connect username in parenthesis allows to get more information - Abdellatif Ait boudad (ai 349B tboudad) - Jan Schädlich (jschaedl) - Lukas Kahwe Smith (lsmith) - - Jérôme Tamarelle (gromnan) - Kevin Bond (kbond) + - Jérôme Tamarelle (gromnan) - Martin Hasoň (hason) - Jeremy Mikola (jmikola) - Jean-François Simon (jfsimon) @@ -52,21 +52,21 @@ The Symfony Connect username in parenthesis allows to get more information - Valentin Udaltsov (vudaltsov) - Vasilij Duško (staff) - Matthias Pigulla (mpdude) + - Antoine Lamirault (alamirault) - Gabriel Ostrolucký (gadelat) - - Antoine Makdessi (amakdessi) - Laurent VOULLEMIER (lvo) + - Antoine Makdessi (amakdessi) - Pierre du Plessis (pierredup) - - Antoine Lamirault (alamirault) - Grégoire Paris (greg0ire) - Jonathan Wage (jwage) + - Mathieu Lechat (mat_the_cat) - Titouan Galopin (tgalopin) - David Maicher (dmaicher) + - Alexander Schranz (alexander-schranz) - Gábor Egyed (1ed) - Mathieu Santostefano (welcomattic) - - Alexander Schranz (alexander-schranz) - Alexandre Salomé (alexandresalome) - William DURAND - - Mathieu Lechat (mat_the_cat) - ornicar - Dany Maillard (maidmaid) - Eriksen Costa @@ -76,10 +76,10 @@ The Symfony Connect username in parenthesis allows to get more information - Francis Besset (francisbesset) - Vasilij Dusko | CREATION - Bulat Shakirzyanov (avalanche123) + - Vincent Langlet (deviling) - Iltar van der Berg - Miha Vrhovnik (mvrhov) - Mathieu Piot (mpiot) - - Vincent Langlet (deviling) - Saša Stamenković (umpirsky) - Alex Pott - Guilhem N (guilhemn) @@ -95,14 +95,14 @@ The Symfony Connect username in parenthesis allows to get more information - Ruud Kamphuis (ruudk) - Henrik Bjørnskov (henrikbjorn) - David Buchmann (dbu) + - Massimiliano Arione (garak) - Andrej Hudec (pulzarraider) - Julien Falque (julienfalque) - - Massimiliano Arione (garak) - Jáchym Toušek (enumag) - Douglas Greenshields (shieldo) + - Mathias Arlaud (mtarld) - Christian Raue - Fran Moreno (franmomu) - - Mathias Arlaud (mtarld) - Graham Campbell (graham) - Michel Weimerskirch (mweimerskirch) - Eric Clemmons (ericclemmons) @@ -117,18 +117,18 @@ The Symfony Connect username in parenthesis allows to get more information - Dariusz Górecki (canni) - Maxime Helias (maxhelias) - Ener-Getick - - Sebastiaan Stok (sstok) - Tugdual Saunier (tucksaun) + - Sebastiaan Stok (sstok) - Jérôme Vasseur (jvasseur) - Ion Bazan (ionbazan) - Rokas Mikalkėnas (rokasm) + - Yanick Witschi (toflar) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) - Daniel Holmes (dholmes) - Toni Uebernickel (havvg) - Bart van den Burg (burgov) - - Yanick Witschi (toflar) - Jordan Alliot (jalliot) - Smaine Milianni (ismail1432) - John Wards (johnwards) @@ -137,6 +137,7 @@ The Symfony Connect username in parenthesis allows to get more information - Antoine Hérault (herzult) - Konstantin.Myakshin - Arman Hosseini (arman) + - gnito-org - Saif Eddin Gmati (azjezz) - Simon Berger - Arnaud Le Blanc (arnaud-lb) @@ -152,6 +153,7 @@ The Symfony Connect username in parenthesis allows to get more information - Chris Wilkinson (thewilkybarkid) - Brice BERNARD (brikou) - Roman Martinuk (a2a4) + - Joel Wurtz (brouznouf) - Gregor Harlan (gharlan) - Baptiste Clavié (talus) - Adrien Brault (adrienbrault) @@ -168,7 +170,6 @@ The Symfony Connect username in parenthesis allows to get more information - Guillaume (guill) - Christopher Hertel (chertel) - Jacob Dreesen (jdreesen) - - Joel Wurtz (brouznouf) - Olivier Dolbeau (odolbeau) - Florian Voutzinos (florianv) - zairig imad (zairigimad) @@ -180,6 +181,7 @@ The Symfony Connect username in parenthesis allows to get more information - HeahDude - Richard van Laak (rvanlaak) - Paráda József (paradajozsef) + - Hubert Lenoir (hubert_lenoir) - Alessandro Lai (jean85) - Alexander Schwenn (xelaris) - Fabien Pennequin (fabienpennequin) @@ -203,7 +205,6 @@ The Symfony Connect username in parenthesis allows to get more information - Tigran Azatyan (tigranazatyan) - Eric GELOEN (gelo) - Matthieu Napoli (mnapoli) - - Hubert Lenoir (hubert_lenoir) - Tomáš Votruba (tomas_votruba) - Joshua Thijssen - Stefano Sala (stefano.sala) @@ -295,7 +296,6 @@ The Symfony Connect username in parenthesis allows to get more information - Samuel NELA (snela) - Romain Monteil (ker0x) - dFayet - - gnito-org - Karoly Gossler (connorhu) - Vincent AUBERT (vincent) - Sebastien Morel (plopix) @@ -304,7 +304,9 @@ The Symfony Connect username in parenthesis allows to get more information - Timothée Barray (tyx) - Sébastien Alfaiate (seb33300) - James Halsall (jaitsu) + - Maximilian Beckers (maxbeckers) - Mikael Pajunen + - Marcin Sikoń (marphi) - Warnar Boekkooi (boekkooi) - Marco Petersen (ocrampete16) - Benjamin Leveque (benji07) @@ -335,6 +337,7 @@ The Symfony Connect username in parenthesis allows to get more information - Urinbayev Shakhobiddin (shokhaa) - Ahmed Raafat - Philippe Segatori + - Allison Guilhem (a_guilhem) - Thibaut Cheymol (tcheymol) - Julien Pauli - Islam Israfilov (islam93) @@ -346,7 +349,6 @@ The Symfony Connect username in parenthesis allows to get more information - Ruben Gonzalez (rubenrua) - Benjamin Dulau (dbenjamin) - Pavel Kirpitsov (pavel-kirpichyov) - - Maximilian Beckers (maxbeckers) - Mathieu Lemoine (lemoinem) - Christian Schmidt - Andreas Hucks (meandmymonkey) @@ -357,7 +359,6 @@ The Symfony Connect username in parenthesis allows to get more information - Clara van Miert - Martin Auswöger - Alexander Menshchikov - - Marcin Sikoń (marphi) - Stepan Anchugov (kix) - bronze1man - sun (sun) @@ -375,11 +376,13 @@ The Symfony Connect username in parenthesis allows to get more information - Kyle - Dominique Bongiraud - Hidde Wieringa (hiddewie) + - Dane Powell - Christopher Davis (chrisguitarguy) - Lukáš Holeczy (holicz) - Michael Lee (zerustech) - Florian Lonqueu-Brochard (florianlb) - Leszek Prabucki (l3l0) + - Giorgio Premi - Emanuele Panzeri (thepanz) - Matthew Smeets - François Zaninotto (fzaninotto) @@ -415,14 +418,12 @@ The Symfony Connect username in parenthesis allows to get more information - Mantis Development - Pablo Lozano (arkadis) - quentin neyrat (qneyrat) - - Dane Powell - Antonio Jose Cerezo (ajcerezo) - Marcin Szepczynski (czepol) - Lescot Edouard (idetox) - Loïc Frémont (loic425) - Rob Frawley 2nd (robfrawley) - Mohammad Emran Hasan (phpfour) - - Allison Guilhem (a_guilhem) - Dmitriy Mamontov (mamontovdmitriy) - Kévin THERAGE (kevin_therage) - Nikita Konstantinov (unkind) @@ -430,7 +431,6 @@ The Symfony Connect username in parenthesis allows to get more information - Francois Zaninotto - Laurent Masforné (heisenberg) - Claude Khedhiri (ck-developer) - - Giorgio Premi - Daniel Tschinder - Christian Schmidt - Alexander Kotynia (olden) @@ -576,6 +576,7 @@ The Symfony Connect username in parenthesis allows to get more information - Grégoire Passault (gregwar) - Jerzy Zawadzki (jzawadzki) - Ismael Ambrosi (iambrosi) + - Samaël Villette (samadu61) - Saif Eddin G - Emmanuel BORGES (eborges78) - siganushka (siganushka) @@ -691,6 +692,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ruben Jacobs (rubenj) - Arkadius Stefanski (arkadius) - Jérémy M (th3mouk) + - Tristan Pouliquen - Terje Bråten - Pierre Rineau - Renan Gonçalves (renan_saddam) @@ -720,6 +722,7 @@ The Symfony Connect username in parenthesis allows to get more information - Eric COURTIAL - Xesxen - ShinDarth + - Phil E. Taylor (philetaylor) - Arun Philip - Stéphane PY (steph_py) - Philipp Kräutli (pkraeutli) @@ -752,10 +755,10 @@ The Symfony Connect username in parenthesis allows to get more information - Hassan Amouhzi - Antonin CLAUZIER (0x346e3730) - Andrei C. (moldman) - - Samaël Villette (samadu61) - Tamas Szijarto - stlrnz - Adrien Wilmet (adrienfr) + - Mathieu Rochette (mathroc) - Alex Bacart - hugovms - Michele Locati @@ -898,6 +901,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jonas Flodén (flojon) - Adrien Lucas (adrienlucas) - Dominik Zogg + - Quentin Devos - Kai Dederichs - Luc Vieillescazes (iamluc) - Thomas Nunninger @@ -956,7 +960,6 @@ The Symfony Connect username in parenthesis allows to get more information - Gigino Chianese (sajito) - Xav` (xavismeh) - Remi Collet - - Mathieu Rochette (mathroc) - Vicent Soria Durá (vicentgodella) - Michael Moravec - Anthony Ferrara @@ -1026,6 +1029,7 @@ The Symfony Connect username in parenthesis allows to get more information - Loïc Faugeron - Aurélien Fredouelle - Pavel Campr (pcampr) + - Markus Staab - Forfarle (forfarle) - Johnny Robeson (johnny) - Kai Eichinger (kai_eichinger) @@ -1267,7 +1271,6 @@ The Symfony Connect username in parenthesis allows to get more information - Szijarto Tamas - Arend Hummeling - Makdessi Alex - - Phil E. Taylor (philetaylor) - Juan Miguel Besada Vidal (soutlink) - dlorek - Stuart Fyfe @@ -1295,7 +1298,6 @@ The Symfony Connect username in parenthesis allows to get more information - Simon Schick (simonsimcity) - Victor Macko (victor_m) - Tristan Roussel - - Quentin Devos - Jorge Vahldick (jvahldick) - Vladimir Mantulo (mantulo) - aim8604 @@ -1303,6 +1305,7 @@ The Symfony Connect username in parenthesis allows to get more information - Maciej Zgadzaj - David Legatt (dlegatt) - Maarten de Boer (mdeboer) + - Alexandre parent - Cameron Porter - Hossein Bukhamsin - Oliver Hoff @@ -1449,7 +1452,6 @@ The Symfony Connect username in parenthesis allows to get more information - Michael Olšavský - Benny Born - Emirald Mateli - - Tristan Pouliquen - Jose Gonzalez - Claudio Zizza - Ivo Valchev @@ -1841,7 +1843,6 @@ The Symfony Connect username in parenthesis allows to get more information - vladyslavstartsev - Kévin - Marc Abramowitz - - Markus Staab - michal - Martijn Evers - Sjoerd Adema @@ -2220,11 +2221,13 @@ The Symfony Connect username in parenthesis allows to get more information - Andrew Tch - Alexander Cheprasov - Rodrigo Díez Villamuera (rodrigodiez) + - Brad Treloar - Stephen Clouse - e-ivanov - Abderrahman DAIF (death_maker) - Yann Rabiller (einenlum) - Jochen Bayer (jocl) + - Constantine Shtompel - VAN DER PUTTE Guillaume (guillaume_vdp) - Patrick Carlo-Hickman - Bruno MATEU @@ -2235,6 +2238,7 @@ The Symfony Connect username in parenthesis allows to get more information - Viacheslav Sychov - Nicolas Sauveur (baishu) - Helmut Hummel (helhum) + - Andrew Neil Forster (krciga22) - Matt Brunt - Carlos Ortega Huetos - Péter Buri (burci) @@ -2285,7 +2289,6 @@ The Symfony Connect username in parenthesis allows to get more information - John Espiritu (johnillo) - Oxan van Leeuwen - pkowalczyk - - Alexandre parent - Soner Sayakci - Max Voloshin (maxvoloshin) - Nicolas Fabre (nfabre) @@ -2400,6 +2403,7 @@ The Symfony Connect username in parenthesis allows to get more information - Claudiu Cristea - Zacharias Luiten - Sebastian Utz + - Oliver Hader - Adrien Gallou (agallou) - Maks Rafalko (bornfree) - Conrad Kleinespel (conradk) @@ -2417,6 +2421,7 @@ The Symfony Connect username in parenthesis allows to get more information - Cédric Lahouste (rapotor) - Samuel Vogel (samuelvogel) - Berat Doğan + - Christian Kolb - Guillaume LECERF - Juanmi Rodriguez Cerón - twifty @@ -2437,6 +2442,8 @@ The Symfony Connect username in parenthesis allows to get more information - Eric Stern - ShiraNai7 - Antal Áron (antalaron) + - Alexander Grimalovsky (flying) + - Ivan Pepelko (pepelko) - Vašek Purchart (vasek-purchart) - Janusz Jabłoński (yanoosh) - Fleuv @@ -2581,6 +2588,7 @@ The Symfony Connect username in parenthesis allows to get more information - Nicolas Schwartz (nicoschwartz) - Tim Jabs (rubinum) - Stéphane Seng (stephaneseng) + - Robert Korulczyk - Jonathan Gough - Benoit Leveque - Benjamin Bender @@ -2652,6 +2660,7 @@ The Symfony Connect username in parenthesis allows to get more information - Martin Schophaus (m_schophaus_adcada) - Martynas Sudintas (martiis) - Anton Sukhachev (mrsuh) + - Vitaliy Zhuk (zhukv) - Marcel Siegert - ryunosuke - Roy de Vos Burchart @@ -2707,6 +2716,7 @@ The Symfony Connect username in parenthesis allows to get more information - Jovan Perovic (jperovic) - Pablo Maria Martelletti (pmartelletti) - Sander van der Vlugt (stranding) + - Maxim Tugaev (tugmaks) - Florian Bogey - Waqas Ahmed - Bert Hekman @@ -3141,6 +3151,7 @@ The Symfony Connect username in parenthesis allows to get more information - Antoine LA - Vyacheslav Slinko - Benjamin Laugueux + - Lane Shukhov - Jakub Chábek - William Pinaud (DocFX) - Johannes @@ -3244,6 +3255,7 @@ The Symfony Connect username in parenthesis allows to get more information - Ahmed Abdulrahman - dinitrol - Penny Leach + - Kevin Mian Kraiker - Yurii K - Richard Trebichavský - g123456789l @@ -3282,6 +3294,7 @@ The Symfony Connect username in parenthesis allows to get more information - ADmad - Nicolas Roudaire - Abdouni Karim (abdounikarim) + - Adrian Günter (adrianguenter) - Andreas Forsblom (aforsblo) - Alex Olmos (alexolmos) - Cedric BERTOLINI (alsciende) 0
    LocaleLocaleFallback localeDomainDomain Times used Message ID Message Preview
    {{ message.locale }}{{ message.fallbackLocale|default('-') }}