diff --git a/typo3/sysext/core/Classes/Routing/PageRouter.php b/typo3/sysext/core/Classes/Routing/PageRouter.php index 18ec33dbc1929536640763ff259393bc911f81ca..d80ce5d061390daefe6a54fd4f4847b6ecf13b0e 100644 --- a/typo3/sysext/core/Classes/Routing/PageRouter.php +++ b/typo3/sysext/core/Classes/Routing/PageRouter.php @@ -346,7 +346,10 @@ class PageRouter implements RouterInterface if ($route->hasOption('deflatedParameters')) { $parameters = $route->getOption('deflatedParameters'); } - $mappableProcessor->generate($route, $parameters); + // skip the route, in case any aspect of it could not be mapped to a value + if ($mappableProcessor->generate($route, $parameters) === false) { + continue; + } // ABSOLUTE_URL is used as default fallback $urlAsString = $generator->generate($routeName, $parameters, $referenceType); $uri = new Uri($urlAsString); diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/EnhancerLinkGenerator/RouteTest.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/EnhancerLinkGenerator/RouteTest.php index dc1c080f188869a8f0c01e4d9c2a4fa947002349..504d67050cfea3834f9a7f8b976abfc9feb8673a 100644 --- a/typo3/sysext/frontend/Tests/Functional/SiteHandling/EnhancerLinkGenerator/RouteTest.php +++ b/typo3/sysext/frontend/Tests/Functional/SiteHandling/EnhancerLinkGenerator/RouteTest.php @@ -453,4 +453,69 @@ final class RouteTest extends AbstractEnhancerLinkGeneratorTestCase { $this->assertGeneratedUriEquals($testSet); } + + public static function outOfScopeValueIsNotMappedDataProvider(): array + { + // variables (applied when invoking expectations) + $variables = Variables::create()->define([ + 'value' => null, // defined via VariableContext + 'routePrefix' => 'enhance', + 'aspectName' => 'value', + ]); + $variableContexts = []; + $enhancers = Builder::create()->declareEnhancers(); + foreach ($enhancers as $enhancer) { + $generatedParams = match ($enhancer->describe()) { + 'Simple' => '&value=[[value]]', + 'Plugin' => '&testing[value]=[[value]]', + 'Extbase' => '&tx_testing_link[action]=index&tx_testing_link[controller]=Link&tx_testing_link[value]=[[value]]', + default => null, + }; + if ($generatedParams === null) { + continue; + } + $variableContexts[] = VariablesContext::create( + Variables::create([ + 'generatedParams' => VariableValue::createUrlEncodedParams($generatedParams, null, '?'), + ]) + )->withRequiredApplicables($enhancer); + } + return Permutation::create($variables) + ->withTargets( + TestSet::create() + ->withMergedApplicables(LanguageContext::create(0)) + ->withTargetPageId(1100) + ->withUrl( + VariableValue::create('https://acme.us/welcome[[generatedParams]]&cHash=') + ), + ) + ->withApplicableSet( + // value `5000` is out of scope for the given range `[1; 100]` + VariablesContext::create(Variables::create(['value' => 5000])), + ) + ->withApplicableItems($enhancers) + ->withApplicableItems($variableContexts) + ->withApplicableSet( + AspectDeclaration::create('StaticRangeMapper')->withConfiguration([ + VariableItem::create('aspectName', [ + 'type' => 'StaticRangeMapper', + 'start' => '1', + 'end' => '100', + ]), + ]) + ) + ->permute() + ->getTargetsForDataProvider(); + } + + /** + * Asserts that value `5000` cannot generate a valid URL, having + * a `StaticRangeMapper` which only allows values in range `[1; 100]`. + */ + #[DataProvider('outOfScopeValueIsNotMappedDataProvider')] + #[Test] + public function outOfScopeValueIsNotMapped(TestSet $testSet): void + { + $this->assertGeneratedUriEquals($testSet, false); + } } diff --git a/typo3/sysext/frontend/Tests/Functional/SiteHandling/Framework/Builder/VariableValue.php b/typo3/sysext/frontend/Tests/Functional/SiteHandling/Framework/Builder/VariableValue.php index 8298de23745023819972491499719556b622562c..843204e2e092be2e951c661995411f6bec95a517 100644 --- a/typo3/sysext/frontend/Tests/Functional/SiteHandling/Framework/Builder/VariableValue.php +++ b/typo3/sysext/frontend/Tests/Functional/SiteHandling/Framework/Builder/VariableValue.php @@ -40,10 +40,19 @@ class VariableValue return new static($value, $defaultVariables); } + public static function createUrlEncodedParams( + string $value, + Variables $defaultVariables = null, + string $prefix = '&' + ): self { + $value = self::urlEncodeParams($value, $prefix); + return self::create($value, $defaultVariables); + } + private function __construct(string $value, Variables $defaultVariables = null) { - $variableNames = $this->extractVariableNames($value); - if (count($variableNames) === 0) { + $variableNames = self::extractVariableNames($value); + if ($variableNames === []) { throw new \LogicException( sprintf( 'Payload did not contain any variables "%s"', @@ -58,9 +67,15 @@ class VariableValue $this->defaultVariables = $defaultVariables; } + public function __toString(): string + { + return $this->value; + } + public function apply(Variables $variables): string { $variables = $variables->withDefined($this->defaultVariables); + $variables = $this->resolveNestedVariables($variables); $this->assertVariableNames($variables); if (!$this->hasAllRequiredDefinedVariableNames($variables)) { @@ -68,12 +83,25 @@ class VariableValue } return str_replace( - array_map([$this, 'wrap'], $variables->keys()), + array_map([self::class, 'wrap'], $variables->keys()), $variables->values(), $this->value ); } + private function resolveNestedVariables(Variables $variables): Variables + { + $target = clone $variables; + foreach ($variables as $key => $value) { + if ($value instanceof self) { + $otherVariables = clone $variables; + unset($otherVariables[$key]); + $target[$key] = $value->apply($otherVariables); + } + } + return $target; + } + private function assertVariableNames(Variables $variables): void { $missingVariableNames = array_diff($this->variableNames, $variables->keys()); @@ -89,7 +117,7 @@ class VariableValue } } - private function extractVariableNames(string $value): array + private static function extractVariableNames(string $value): array { if (!preg_match_all('#\[\[(?P<variableName>[^\[\]]+)\]\]#', $value, $matches)) { return []; @@ -97,8 +125,19 @@ class VariableValue return array_unique($matches['variableName']); } - private function wrap(string $item): string + private static function wrap(string $item): string { return '[[' . $item . ']]'; } + + private static function urlEncodeParams(string $value, string $prefix = '&'): string + { + $variableNames = self::extractVariableNames($value); + $variableItems = array_map([self::class, 'wrap'], $variableNames); + $substitutes = array_map(static fn(): string => bin2hex(random_bytes(20)), $variableNames); + $value = str_replace($variableItems, $substitutes, $value); + parse_str($value, $params); + $value = http_build_query($params, '', '&', PHP_QUERY_RFC3986); + return $prefix . str_replace($substitutes, $variableItems, $value); + } }