From 3370b77a2e49bd176c9ef2b1fd0fe7e0ab856632 Mon Sep 17 00:00:00 2001 From: Christian Wolfram Date: Tue, 22 Sep 2026 10:26:25 +0200 Subject: [PATCH 1/2] [BUGFIX] Escape slashes in marker replacement Markers whose key contains a slash were never replaced in the frontend output. The regular expression that searches for markers is built with a slash as delimiter, but the marker keys were escaped without telling the escaping function about that delimiter. A key such as "foo/bar" therefore ended the pattern early, and the resulting expression either failed or matched the wrong text. Editors saw the raw marker instead of its value. The marker keys are now escaped with the slash delimiter in mind, so every character of a key is treated as literal text. Markers containing slashes, dots or other characters with a special meaning in regular expressions are replaced like any other marker. --- Classes/Service/VariablesService.php | 12 +++- Tests/Unit/Service/VariablesServiceTest.php | 74 +++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 Tests/Unit/Service/VariablesServiceTest.php diff --git a/Classes/Service/VariablesService.php b/Classes/Service/VariablesService.php index a9aa907..5afcc9b 100644 --- a/Classes/Service/VariablesService.php +++ b/Classes/Service/VariablesService.php @@ -87,7 +87,7 @@ private function replaceMarkersInText( MarkerCollection $markerCollection, string &$text ): void { - $markerRegexp = '/(' . implode('|', array_map(preg_quote(...), $markerCollection->getMarkerKeys())) . ')/'; + $markerRegexp = $this->buildMarkerRegExp($markerCollection); $loops = 0; @@ -169,6 +169,16 @@ private function getMarkers( return $markers; } + private function buildMarkerRegExp(MarkerCollection $markerCollection): string + { + $quotedMarkerKeys = []; + foreach ($markerCollection->getMarkerKeys() as $markerKey) { + $quotedMarkerKeys[] = preg_quote($markerKey, '/'); + } + + return '/(' . implode('|', $quotedMarkerKeys) . ')/'; + } + private function setCacheTags( ServerRequestInterface $request, ): void { diff --git a/Tests/Unit/Service/VariablesServiceTest.php b/Tests/Unit/Service/VariablesServiceTest.php new file mode 100644 index 0000000..85b29e8 --- /dev/null +++ b/Tests/Unit/Service/VariablesServiceTest.php @@ -0,0 +1,74 @@ + + */ + public static function markerKeyDataProvider(): iterable + { + yield 'plain key' => ['key', 'replacement', 'before {{key}} after', 'before replacement after']; + yield 'key containing a slash' => ['foo/bar', 'replacement', 'before {{foo/bar}} after', 'before replacement after']; + yield 'key containing a regular expression special character' => ['foo.bar', 'replacement', 'before {{foo.bar}} after', 'before replacement after']; + } + + #[Test] + #[DataProvider('markerKeyDataProvider')] + public function replacesMarkerRegardlessOfSpecialCharactersInKey( + string $markerKey, + string $replacement, + string $text, + string $expected, + ): void { + $markerCollection = new MarkerCollection(); + $markerCollection->add(new Marker(1, $markerKey, $replacement)); + + $this->replaceMarkersInStructure($markerCollection, $text); + + self::assertSame($expected, $text); + } + + private function replaceMarkersInStructure(MarkerCollection $markerCollection, mixed &$structure): void + { + $extensionConfiguration = $this->createMock(ExtensionConfiguration::class); + $extensionConfiguration->method('get')->willReturn(false); + + $subject = new VariablesService( + $extensionConfiguration, + $this->createMock(Context::class), + $this->createMock(ConnectionPool::class), + ); + + $reflectionMethod = new \ReflectionMethod($subject, 'replaceMarkersInStructure'); + $reflectionMethod->invokeArgs($subject, [$markerCollection, &$structure]); + } +} From c42a79d58f3bb80830436e1938cb76a26acc61e7 Mon Sep 17 00:00:00 2001 From: Christian Wolfram Date: Tue, 22 Sep 2026 10:51:59 +0200 Subject: [PATCH 2/2] [TASK] Add guard for empty markerCollection --- Classes/Service/VariablesService.php | 45 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/Classes/Service/VariablesService.php b/Classes/Service/VariablesService.php index 5afcc9b..91ce70a 100644 --- a/Classes/Service/VariablesService.php +++ b/Classes/Service/VariablesService.php @@ -87,30 +87,31 @@ private function replaceMarkersInText( MarkerCollection $markerCollection, string &$text ): void { - $markerRegexp = $this->buildMarkerRegExp($markerCollection); - - $loops = 0; - - while (preg_match($markerRegexp, $text) && $loops++ < self::MAXIMUM_LOOP_COUNT) { - foreach ($markerCollection as $marker) { - $newContent = str_replace( - $marker->getMarkerWithBrackets(), - $marker->replacement, - $text - ); + if ($markerCollection->isEmpty() === false) { + $markerRegexp = $this->buildMarkerRegExp($markerCollection); + $loops = 0; + + while (preg_match($markerRegexp, $text) && $loops++ < self::MAXIMUM_LOOP_COUNT) { + foreach ($markerCollection as $marker) { + $newContent = str_replace( + $marker->getMarkerWithBrackets(), + $marker->replacement, + $text + ); + + if ($newContent === $text) { + continue; + } - if ($newContent === $text) { - continue; + // Assign a cache key associated with the marker + $this->cacheTags->add( + CacheKeyUtility::getCacheKey( + $marker->getMarkerWithBrackets() + ) + ); + $this->usedMarkerKeys[] = $marker->key; + $text = $newContent; } - - // Assign a cache key associated with the marker - $this->cacheTags->add( - CacheKeyUtility::getCacheKey( - $marker->getMarkerWithBrackets() - ) - ); - $this->usedMarkerKeys[] = $marker->key; - $text = $newContent; } }