diff --git a/typo3/sysext/core/Classes/Utility/GeneralUtility.php b/typo3/sysext/core/Classes/Utility/GeneralUtility.php index 44725e4fcebb..195d267dc655 100644 --- a/typo3/sysext/core/Classes/Utility/GeneralUtility.php +++ b/typo3/sysext/core/Classes/Utility/GeneralUtility.php @@ -1799,6 +1799,12 @@ public static function getUrl($url, $includeHeader = 0, $requestHeaders = null, /** @var RequestFactory $requestFactory */ $requestFactory = static::makeInstance(RequestFactory::class); if (is_array($requestHeaders)) { + // Check is $requestHeaders is an associative array or not + if (count(array_filter(array_keys($requestHeaders), 'is_string')) === 0) { + trigger_error('Request headers as colon-separated string are deprecated, use an associative array instead.', E_USER_DEPRECATED); + // Convert cURL style lines of headers to Guzzle key/value(s) pairs. + $requestHeaders = static::splitHeaderLines($requestHeaders); + } $configuration = ['headers' => $requestHeaders]; } else { $configuration = []; @@ -1870,6 +1876,38 @@ public static function getUrl($url, $includeHeader = 0, $requestHeaders = null, return $content; } + /** + * Split an array of MIME header strings into an associative array. + * Multiple headers with the same name have their values merged as an array. + * + * @static + * @param array $headers List of headers, eg. ['Foo: Bar', 'Foo: Baz'] + * @return array Key/Value(s) pairs of headers, eg. ['Foo' => ['Bar', 'Baz']] + */ + protected static function splitHeaderLines(array $headers): array + { + $newHeaders = []; + foreach ($headers as $header) { + $parts = preg_split('/:[ \t]*/', $header, 2, PREG_SPLIT_NO_EMPTY); + if (count($parts) !== 2) { + continue; + } + $key = &$parts[0]; + $value = &$parts[1]; + if (array_key_exists($key, $newHeaders)) { + if (is_array($newHeaders[$key])) { + $newHeaders[$key][] = $value; + } else { + $prevValue = &$newHeaders[$key]; + $newHeaders[$key] = [$prevValue, $value]; + } + } else { + $newHeaders[$key] = $value; + } + } + return $newHeaders; + } + /** * Writes $content to the file $file * diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-84171-AddingGeneralUtilitygetUrlRequestHeadersAsNon-associativeArrayAreDeprecated.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-84171-AddingGeneralUtilitygetUrlRequestHeadersAsNon-associativeArrayAreDeprecated.rst new file mode 100644 index 000000000000..3c29731efcf8 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-84171-AddingGeneralUtilitygetUrlRequestHeadersAsNon-associativeArrayAreDeprecated.rst @@ -0,0 +1,42 @@ +.. include:: ../../Includes.txt + +========================================================================================================== +Deprecation: #84171 - Adding GeneralUtility::getUrl RequestHeaders as non-associative array are deprecated +========================================================================================================== + +See :issue:`84171` + +Description +=========== + +RequestHeaders passed to getUrl as string (format `Header:Value`) have been deprecated. Associative arrays should be used instead. + + +Impact +====== + +Using `GeneralUtility::getUrl` request headers in a non-associative way will trigger an `E_USER_DEPRECATED` PHP error. + + +Affected Installations +====================== + +All using request headers for `GeneralUtility::getUrl` in a non-associative way. + + +Migration +========= + +Use associative arrays, for example: + +.. code-block:: php + + $headers = ['Content-Language: de-DE']; + +will become + +.. code-block:: php + + $headers = ['Content-Language' => 'de-DE']; + +.. index:: PHP-API, NotScanned \ No newline at end of file diff --git a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php index b5f3587799ff..50f1168e0a9d 100644 --- a/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php +++ b/typo3/sysext/core/Tests/Unit/Utility/GeneralUtilityTest.php @@ -17,7 +17,11 @@ use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamDirectory; use org\bovigo\vfs\vfsStreamWrapper; +use Prophecy\Argument; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; use Psr\Log\LoggerInterface; +use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Package\Package; use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\CMS\Core\Tests\Unit\Utility\AccessibleProxies\ExtensionManagementUtilityAccessibleProxy; @@ -4786,4 +4790,34 @@ public function idnaEncodeDataProvider() ] ]; } + + public function splitHeaderLinesDataProvider(): array + { + return [ + 'multi-line headers' => [ + ['Content-Type' => 'multipart/form-data; boundary=something', 'Content-Language' => 'de-DE, en-CA'], + ['Content-Type' => 'multipart/form-data; boundary=something', 'Content-Language' => 'de-DE, en-CA'], + ] + ]; + } + + /** + * @test + * @dataProvider splitHeaderLinesDataProvider + * @param array $headers + * @param array $expectedHeaders + */ + public function splitHeaderLines(array $headers, array $expectedHeaders): void + { + $stream = $this->prophesize(StreamInterface::class); + $response = $this->prophesize(ResponseInterface::class); + $response->getBody()->willReturn($stream); + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->request(Argument::cetera())->willReturn($response); + + GeneralUtility::addInstance(RequestFactory::class, $requestFactory->reveal()); + GeneralUtility::getUrl('http://example.com', 0, $headers); + + $requestFactory->request(Argument::any(), Argument::any(), ['headers' => $expectedHeaders])->shouldHaveBeenCalled(); + } } diff --git a/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php b/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php new file mode 100644 index 000000000000..e8d847a3436a --- /dev/null +++ b/typo3/sysext/core/Tests/UnitDeprecated/Utility/GeneralUtilityTest.php @@ -0,0 +1,70 @@ + [ + ['Content-Security-Policy:default-src \'self\'; img-src https://*; child-src \'none\';'], + ['Content-Security-Policy' => 'default-src \'self\'; img-src https://*; child-src \'none\';'] + ], + 'one-line, multiple headers' => [ + [ + 'Content-Security-Policy:default-src \'self\'; img-src https://*; child-src \'none\';', + 'Content-Security-Policy-Report-Only:default-src https:; report-uri /csp-violation-report-endpoint/' + ], + [ + 'Content-Security-Policy' => 'default-src \'self\'; img-src https://*; child-src \'none\';', + 'Content-Security-Policy-Report-Only' => 'default-src https:; report-uri /csp-violation-report-endpoint/' + ] + ] + ]; + } + + /** + * @test + * @dataProvider splitHeaderLinesDataProvider + * @param array $headers + * @param array $expectedHeaders + */ + public function splitHeaderLines(array $headers, array $expectedHeaders): void + { + $stream = $this->prophesize(StreamInterface::class); + $response = $this->prophesize(ResponseInterface::class); + $response->getBody()->willReturn($stream); + $requestFactory = $this->prophesize(RequestFactory::class); + $requestFactory->request(Argument::cetera())->willReturn($response); + + GeneralUtility::addInstance(RequestFactory::class, $requestFactory->reveal()); + GeneralUtility::getUrl('http://example.com', 0, $headers); + + $requestFactory->request(Argument::any(), Argument::any(), ['headers' => $expectedHeaders]) + ->shouldHaveBeenCalled(); + } +} diff --git a/typo3/sysext/frontend/Classes/Controller/ErrorController.php b/typo3/sysext/frontend/Classes/Controller/ErrorController.php index 6d0f1b5d160a..c6c816a82850 100644 --- a/typo3/sysext/frontend/Classes/Controller/ErrorController.php +++ b/typo3/sysext/frontend/Classes/Controller/ErrorController.php @@ -182,8 +182,8 @@ protected function handlePageError($errorHandler, string $header = '', string $r } // Prepare headers $requestHeaders = [ - 'User-agent: ' . GeneralUtility::getIndpEnv('HTTP_USER_AGENT'), - 'Referer: ' . GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL') + 'User-agent' => GeneralUtility::getIndpEnv('HTTP_USER_AGENT'), + 'Referer' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL') ]; $report = []; $res = GeneralUtility::getUrl($errorHandler, 1, $requestHeaders, $report);