From 831c3df79ce8215765dd34fd771d06f083ccaa6f Mon Sep 17 00:00:00 2001 From: Oliver Hader <oliver@typo3.org> Date: Tue, 27 Jun 2023 16:18:14 +0200 Subject: [PATCH] [TASK] Add shared cache for VariableProcessor VariableProcessor::addHash() is called to convert URI path variables that don't comply with certain rules to a hash, which does. First of all, variables must not exceed a certain length of 32, second of all, variables must only contain word characters aka "\w" or "[A-Za-z0-9_]". So whenever a variable is too long or contains invalid characters, it is converted to an md5 hash which complies with both rules. It has been reported that the checks, whether to generate a hash and the hashing would have a very noticable performance impact. This change introduces as specific runtime cache for VariableProcessor that is shared with any other new instance. This way, the necessity whether a hash is required and the hash representation of a value can be resolved from this cache. For instance, this would speed up scenarios that are enerating large product lists and generate, when every invocation with a new UID would have triggered a new hash check and assignment. Resolves: #100974 Releases: main, 12.4 Change-Id: I19e1d138a79792cc05a6a908f1ee5f87236a65bd Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/79710 Tested-by: Oliver Hader <oliver.hader@typo3.org> Reviewed-by: Oliver Hader <oliver.hader@typo3.org> Tested-by: core-ci <typo3@b13.com> --- .../Routing/Enhancer/AbstractEnhancer.php | 3 +- .../Routing/Enhancer/VariableProcessor.php | 50 +++++++++++++++---- .../Enhancer/VariableProcessorCache.php | 36 +++++++++++++ typo3/sysext/core/Configuration/Services.yaml | 7 +++ .../Enhancer/VariableProcessorTest.php | 11 ++-- 5 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 typo3/sysext/core/Classes/Routing/Enhancer/VariableProcessorCache.php diff --git a/typo3/sysext/core/Classes/Routing/Enhancer/AbstractEnhancer.php b/typo3/sysext/core/Classes/Routing/Enhancer/AbstractEnhancer.php index e314a1ad1d81..a3c5e649fe2f 100644 --- a/typo3/sysext/core/Classes/Routing/Enhancer/AbstractEnhancer.php +++ b/typo3/sysext/core/Classes/Routing/Enhancer/AbstractEnhancer.php @@ -20,6 +20,7 @@ namespace TYPO3\CMS\Core\Routing\Enhancer; use TYPO3\CMS\Core\Routing\Aspect\AspectInterface; use TYPO3\CMS\Core\Routing\Aspect\ModifiableAspectInterface; use TYPO3\CMS\Core\Routing\Route; +use TYPO3\CMS\Core\Utility\GeneralUtility; /** * Abstract Enhancer, useful for custom enhancers @@ -181,7 +182,7 @@ abstract class AbstractEnhancer implements EnhancerInterface if (isset($this->variableProcessor)) { return $this->variableProcessor; } - return $this->variableProcessor = new VariableProcessor(); + return $this->variableProcessor = GeneralUtility::makeInstance(VariableProcessor::class); } /** diff --git a/typo3/sysext/core/Classes/Routing/Enhancer/VariableProcessor.php b/typo3/sysext/core/Classes/Routing/Enhancer/VariableProcessor.php index ff59e9b0b921..3b5330e3fafb 100644 --- a/typo3/sysext/core/Classes/Routing/Enhancer/VariableProcessor.php +++ b/typo3/sysext/core/Classes/Routing/Enhancer/VariableProcessor.php @@ -36,25 +36,53 @@ class VariableProcessor */ protected $nestedValues = []; + public function __construct(private readonly VariableProcessorCache $cache) + { + } + protected function addHash(string $value): string { - if (strlen($value) < 31 && !preg_match('#[^\w]#', $value)) { + if (!$this->requiresHashing($value)) { return $value; } - // removing one bit, e.g. for enforced route prefix `{!value}` - $hash = substr(md5($value), 0, -1); - // Symfony Route Compiler requires first literal to be non-integer - if ($hash[0] === (string)(int)$hash[0]) { - $hash[0] = str_replace( - range('0', '9'), - range('o', 'x'), - $hash[0] - ); - } + // generate hash (fetch from cache, if available) + $hash = $this->generateHash($value); + // store hash locally (indicator, that this value was processed) $this->hashes[$hash] = $value; return $hash; } + /** + * Determines whether a parameter value requires hashing. + * This is the case if the value has 31+ chars (Symfony has a limitation of 32 chars), + * or if the value contains any non-word characters besides `[A-Za-z0-9_]`, such as `@`. + */ + protected function requiresHashing(string $value): bool + { + if (!isset($this->cache->requiresHashing[$value])) { + $this->cache->requiresHashing[$value] = strlen($value) >= 31 || preg_match('#[^\w]#', $value) > 0; + } + return $this->cache->requiresHashing[$value]; + } + + protected function generateHash(string $value): string + { + if (!isset($this->cache->hashes[$value])) { + // remove one char, which might be used as enforced route prefix `{!value}` + $hash = substr(md5($value), 0, -1); + // Symfony Route Compiler requires the first literal to be non-integer + if ($hash[0] === (string)(int)$hash[0]) { + $hash[0] = str_replace( + range('0', '9'), + range('o', 'x'), + $hash[0] + ); + } + $this->cache->hashes[$value] = $hash; + } + return $this->cache->hashes[$value]; + } + /** * @throws \OutOfRangeException */ diff --git a/typo3/sysext/core/Classes/Routing/Enhancer/VariableProcessorCache.php b/typo3/sysext/core/Classes/Routing/Enhancer/VariableProcessorCache.php new file mode 100644 index 000000000000..883cde58207d --- /dev/null +++ b/typo3/sysext/core/Classes/Routing/Enhancer/VariableProcessorCache.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +namespace TYPO3\CMS\Core\Routing\Enhancer; + +/** + * Shared cache among multiple `VariableProcessor` instances + * + * @internal + */ +class VariableProcessorCache +{ + /** + * @var array<string, bool> + */ + public array $requiresHashing = []; + + /** + * @var array<string, string> + */ + public array $hashes = []; +} diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml index 6399f0ad83a3..3d0ea395b6bf 100644 --- a/typo3/sysext/core/Configuration/Services.yaml +++ b/typo3/sysext/core/Configuration/Services.yaml @@ -147,6 +147,13 @@ services: TYPO3\CMS\Core\Routing\RequestContextFactory: public: true + TYPO3\CMS\Core\Routing\Enhancer\VariableProcessor: + public: true + shared: false + + TYPO3\CMS\Core\Routing\Enhancer\VariableProcessorCache: + shared: true + # FAL security checks for backend users TYPO3\CMS\Core\Resource\Security\StoragePermissionsAspect: tags: diff --git a/typo3/sysext/core/Tests/Unit/Routing/Enhancer/VariableProcessorTest.php b/typo3/sysext/core/Tests/Unit/Routing/Enhancer/VariableProcessorTest.php index e731e16489cc..3c17b33eab85 100644 --- a/typo3/sysext/core/Tests/Unit/Routing/Enhancer/VariableProcessorTest.php +++ b/typo3/sysext/core/Tests/Unit/Routing/Enhancer/VariableProcessorTest.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Core\Tests\Unit\Routing\Enhancer; use TYPO3\CMS\Core\Routing\Enhancer\VariableProcessor; +use TYPO3\CMS\Core\Routing\Enhancer\VariableProcessorCache; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; final class VariableProcessorTest extends UnitTestCase @@ -127,7 +128,7 @@ final class VariableProcessorTest extends UnitTestCase */ public function isRoutePathProcessed(?string $namespace, array $arguments, string $inflatedRoutePath, string $deflatedRoutePath): void { - $subject = new VariableProcessor(); + $subject = new VariableProcessor(new VariableProcessorCache()); self::assertSame( $deflatedRoutePath, $subject->deflateRoutePath($inflatedRoutePath, $namespace, $arguments) @@ -166,7 +167,7 @@ final class VariableProcessorTest extends UnitTestCase */ public function parametersAreProcessed(array $arguments, array $deflatedParameters): void { - $subject = new VariableProcessor(); + $subject = new VariableProcessor(new VariableProcessorCache()); $inflatedParameters = ['a' => 'a', 'first' => ['aa' => 'aa', 'second' => ['aaa' => 'aaa', '@any' => '@any']]]; self::assertEquals( $deflatedParameters, @@ -242,7 +243,7 @@ final class VariableProcessorTest extends UnitTestCase */ public function namespaceParametersAreProcessed(string $namespace, array $arguments, array $deflatedParameters): void { - $subject = new VariableProcessor(); + $subject = new VariableProcessor(new VariableProcessorCache()); $inflatedParameters = ['a' => 'a', 'first' => ['aa' => 'aa', 'second' => ['aaa' => 'aaa', '@any' => '@any']]]; self::assertEquals( $deflatedParameters, @@ -330,7 +331,7 @@ final class VariableProcessorTest extends UnitTestCase */ public function keysAreDeflated(?string $namespace, array $arguments, array $deflatedKeys): void { - $subject = new VariableProcessor(); + $subject = new VariableProcessor(new VariableProcessorCache()); $inflatedKeys = ['a' => 'a', 'b' => 'b', 'c' => ['d' => 'd', 'e' => 'e']]; self::assertEquals( $deflatedKeys, @@ -350,6 +351,6 @@ final class VariableProcessorTest extends UnitTestCase { $this->expectException(\OutOfRangeException::class); $this->expectExceptionCode(1537633463); - (new VariableProcessor())->inflateKeys($deflatedKeys, $namespace, $arguments); + (new VariableProcessor(new VariableProcessorCache()))->inflateKeys($deflatedKeys, $namespace, $arguments); } } -- GitLab