TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
<?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;
|
||||
|
||||
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
|
||||
*/
|
||||
abstract class AbstractEnhancer implements EnhancerInterface
|
||||
{
|
||||
/**
|
||||
* @var AspectInterface[]
|
||||
*/
|
||||
protected array $aspects = [];
|
||||
|
||||
/**
|
||||
* @var VariableProcessor|null
|
||||
*/
|
||||
protected $variableProcessor;
|
||||
|
||||
/**
|
||||
* @param AspectInterface[] $aspects
|
||||
* @param string|null $namespace
|
||||
*/
|
||||
protected function applyRouteAspects(Route $route, array $aspects, ?string $namespace = null)
|
||||
{
|
||||
if (empty($aspects)) {
|
||||
return;
|
||||
}
|
||||
$aspects = $this->getVariableProcessor()
|
||||
->deflateKeys($aspects, $namespace, $route->getArguments());
|
||||
$route->setAspects($aspects);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $namespace
|
||||
*/
|
||||
protected function applyRequirements(Route $route, array $requirements, ?string $namespace = null)
|
||||
{
|
||||
$requirements = $this->getVariableProcessor()
|
||||
->deflateKeys($requirements, $namespace, $route->getArguments());
|
||||
// only keep requirements that are actually part of the current route path
|
||||
$requirements = $this->filterValuesByPathVariables($route, $requirements);
|
||||
// Symfony's behavior on applying pattern for parameters just concerns values
|
||||
// to be passed either to URL or to internal parameters - they are always the
|
||||
// same, without any transformation.
|
||||
//
|
||||
// TYPO3 extends ("enhances") this behavior by making a difference between values
|
||||
// for generation (resulting in a URL) and matching (resulting in query parameters)
|
||||
// having the following implications and meaning:
|
||||
//
|
||||
// + since requirements in classic Symfony focus on parameters in URLs
|
||||
// and aspects define a mapping between URL part (e.g. 'some-example-news')
|
||||
// and the corresponding internal argument (e.g. 'tx_news_pi1[news]=123')
|
||||
// + thus, the requirement definition cannot be used for resolving and generating
|
||||
// a route at the same time (it would have to be e.g. `[\w_._]+` AND `\d+`)
|
||||
//
|
||||
// Symfony's default regular expression pattern `[^/]+` (see
|
||||
// `RouteCompiler::compilePattern()`) has to be overridden with `.+` to
|
||||
// allow URI parameters like `some-example-news/january` as well.
|
||||
//
|
||||
// Existing `requirements` for TYPO3 route enhancers are not modified, only those
|
||||
// that are not defined and would use Symfony's default pattern.
|
||||
$requirements = $this->defineValuesByAspect($route, $requirements, '.+');
|
||||
$route->setRequirements($requirements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies variables that are considered static (not having `&cHash=...` applied),
|
||||
* without having the demand to define a custom `StaticMappableAspectInterface`
|
||||
* to fake the behavior.
|
||||
*
|
||||
* However:
|
||||
* + in case there's an aspect defined for a variable, it will be skipped (aspects take precedence)
|
||||
* + in case not requirement is defined for a variable, it will be skipped (avoiding weak definitions)
|
||||
*
|
||||
* @param array<non-empty-string, bool> $staticVariables option values
|
||||
*/
|
||||
protected function applyStaticVariables(Route $route, array $staticVariables, ?string $namespace = null): void
|
||||
{
|
||||
// skip definitions that are not explicitly set to `true`
|
||||
$staticVariables = array_filter($staticVariables, static fn($definition) => $definition === true);
|
||||
$staticVariables = $this->getVariableProcessor()
|
||||
->deflateKeys($staticVariables, $namespace, $route->getArguments());
|
||||
// only keep static variables that are actually part of the current route path
|
||||
$staticVariables = $this->filterValuesByPathVariables($route, $staticVariables);
|
||||
// skip definitions that already have an aspect defined (aspects take precedence)
|
||||
$staticVariables = array_diff_key($staticVariables, $route->getAspects());
|
||||
// skip definitions that not have any requirement defined (avoiding weak definitions)
|
||||
$staticVariables = array_intersect_key($staticVariables, $route->getRequirements());
|
||||
$route->setOption('_static', $staticVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Only keeps values that actually have been used as variables in route path.
|
||||
*
|
||||
* + routePath: '/list/{page}' ('page' used as variable in route path)
|
||||
* + values: ['entity' => 'entity...', 'page' => 'page...', 'other' => 'other...']
|
||||
* + result: ['page' => 'page...']
|
||||
*
|
||||
* @param Route $route
|
||||
* @param array $values
|
||||
*/
|
||||
protected function filterValuesByPathVariables(Route $route, array $values): array
|
||||
{
|
||||
return array_intersect_key(
|
||||
$values,
|
||||
array_flip($route->compile()->getPathVariables())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides items having an aspect definition with a given
|
||||
* $overrideValue in target $targetValue array.
|
||||
*/
|
||||
protected function overrideValuesByAspect(Route $route, array $values, string $targetValue): array
|
||||
{
|
||||
foreach (array_keys($route->getAspects()) as $variableName) {
|
||||
$values[$variableName] = $targetValue;
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define items having an aspect definition in case they are not defined
|
||||
* with a given $targetValue in target $targetValue array.
|
||||
*/
|
||||
protected function defineValuesByAspect(Route $route, array $values, string $targetValue): array
|
||||
{
|
||||
foreach (array_keys($route->getAspects()) as $variableName) {
|
||||
if (isset($values[$variableName])) {
|
||||
continue;
|
||||
}
|
||||
$values[$variableName] = $targetValue;
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the route path to add the variable names with the aspects, e.g.
|
||||
*
|
||||
* + `/{locale_modifier}/{product_title}` -> `/products/{product_title}`
|
||||
* + `/{!locale_modifier}/{product_title}` -> `/products/{product_title}`
|
||||
*
|
||||
* @param string $routePath
|
||||
*/
|
||||
protected function modifyRoutePath(string $routePath): string
|
||||
{
|
||||
$substitutes = [];
|
||||
foreach ($this->aspects as $variableName => $aspect) {
|
||||
if (!$aspect instanceof ModifiableAspectInterface) {
|
||||
continue;
|
||||
}
|
||||
$value = $aspect->modify();
|
||||
if ($value !== null) {
|
||||
$substitutes['{' . $variableName . '}'] = $value;
|
||||
$substitutes['{!' . $variableName . '}'] = $value;
|
||||
}
|
||||
}
|
||||
return str_replace(
|
||||
array_keys($substitutes),
|
||||
array_values($substitutes),
|
||||
$routePath
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves type from processed route and modifies remaining query parameters.
|
||||
*
|
||||
* @param array $remainingQueryParameters reference to remaining query parameters
|
||||
*/
|
||||
protected function resolveType(Route $route, array &$remainingQueryParameters): string
|
||||
{
|
||||
$type = $remainingQueryParameters['type'] ?? 0;
|
||||
$decoratedParameters = $route->getOption('_decoratedParameters');
|
||||
if (isset($decoratedParameters['type'])) {
|
||||
$type = $decoratedParameters['type'];
|
||||
unset($decoratedParameters['type']);
|
||||
$remainingQueryParameters = array_replace_recursive(
|
||||
$remainingQueryParameters,
|
||||
$decoratedParameters
|
||||
);
|
||||
}
|
||||
return (string)$type;
|
||||
}
|
||||
|
||||
protected function getVariableProcessor(): VariableProcessor
|
||||
{
|
||||
if (isset($this->variableProcessor)) {
|
||||
return $this->variableProcessor;
|
||||
}
|
||||
return $this->variableProcessor = GeneralUtility::makeInstance(VariableProcessor::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setAspects(array $aspects): void
|
||||
{
|
||||
$this->aspects = $aspects;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAspects(): array
|
||||
{
|
||||
return $this->aspects;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Decorates a route (or routes within a collection) with additional parameters.
|
||||
*/
|
||||
interface DecoratingEnhancerInterface extends EnhancerInterface
|
||||
{
|
||||
/**
|
||||
* Gets pattern that can be used to redecorate (undecorate)
|
||||
* a potential previously decorated route path.
|
||||
*
|
||||
* Example:
|
||||
* + route path: 'first/second.html'
|
||||
* + redecoration pattern: '(?:\.html|\.json)$'
|
||||
* -> 'first/second' might be the redecorated route path after
|
||||
* applying the redecoration pattern to preg_match/preg_replace
|
||||
*
|
||||
* @return string regular expression pattern
|
||||
*/
|
||||
public function getRoutePathRedecorationPattern(): string;
|
||||
|
||||
/**
|
||||
* Decorates route collection to be processed during URL resolving.
|
||||
* Executed before invoking routing enhancers.
|
||||
*
|
||||
* @param string $routePath URL path
|
||||
*/
|
||||
public function decorateForMatching(RouteCollection $collection, string $routePath): void;
|
||||
|
||||
/**
|
||||
* Decorates route collection during URL URL generation.
|
||||
* Executed before invoking routing enhancers.
|
||||
*
|
||||
* @param array $parameters query parameters
|
||||
*/
|
||||
public function decorateForGeneration(RouteCollection $collection, array $parameters): void;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Creates enhancers
|
||||
*/
|
||||
class EnhancerFactory
|
||||
{
|
||||
/**
|
||||
* @var array of all class names that need to be EnhancerInterfaces when instantiated.
|
||||
*/
|
||||
protected $availableEnhancers;
|
||||
|
||||
/**
|
||||
* EnhancerFactory constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->availableEnhancers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['enhancers'] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
public function create(string $type, array $settings): EnhancerInterface
|
||||
{
|
||||
if (empty($type)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'Enhancer type cannot be empty',
|
||||
1537298284
|
||||
);
|
||||
}
|
||||
if (!isset($this->availableEnhancers[$type])) {
|
||||
throw new \OutOfRangeException(
|
||||
sprintf('No enhancer found for %s', $type),
|
||||
1537277222
|
||||
);
|
||||
}
|
||||
unset($settings['type']);
|
||||
$className = $this->availableEnhancers[$type];
|
||||
/** @var EnhancerInterface $enhancer */
|
||||
$enhancer = GeneralUtility::makeInstance($className, $settings);
|
||||
return $enhancer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Routing\Aspect\AspectInterface;
|
||||
|
||||
/**
|
||||
* Base interface for enhancers, which can be decorators for adding parameters,
|
||||
* or routing enhancers which adds variants to a page.
|
||||
*/
|
||||
interface EnhancerInterface
|
||||
{
|
||||
/**
|
||||
* @param AspectInterface[] $aspects
|
||||
*/
|
||||
public function setAspects(array $aspects): void;
|
||||
|
||||
/**
|
||||
* @return AspectInterface[]
|
||||
*/
|
||||
public function getAspects(): array;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Interface asserting that enhancer is capable of inflating parameters.
|
||||
*/
|
||||
interface InflatableEnhancerInterface
|
||||
{
|
||||
public function inflateParameters(array $parameters, array $internals = []): array;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Routing\Route;
|
||||
use TYPO3\CMS\Core\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Resolves a static list (like page.typeNum) against a file pattern. Usually added on the very last part
|
||||
* of the URL.
|
||||
* It is important that the PageType Enhancer is executed at the very end in your configuration, as it modifies
|
||||
* EXISTING route variants.
|
||||
*
|
||||
* routeEnhancers:
|
||||
* PageTypeSuffix:
|
||||
* type: PageType
|
||||
* default: ''
|
||||
* index: 'index'
|
||||
* map:
|
||||
* '.html': 1
|
||||
* 'menu.json': 13
|
||||
*/
|
||||
class PageTypeDecorator extends AbstractEnhancer implements DecoratingEnhancerInterface
|
||||
{
|
||||
protected const ROUTE_PATH_DELIMITERS = ['.', '-', '_', '/'];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $default;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $index;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $map;
|
||||
|
||||
public function __construct(array $configuration)
|
||||
{
|
||||
$default = $configuration['default'] ?? '';
|
||||
$index = $configuration['index'] ?? 'index';
|
||||
$map = $configuration['map'] ?? null;
|
||||
|
||||
if (!is_string($default)) {
|
||||
throw new \InvalidArgumentException('default must be string', 1538327508);
|
||||
}
|
||||
if (!is_string($index)) {
|
||||
throw new \InvalidArgumentException('index must be string', 1538327509);
|
||||
}
|
||||
if (!is_array($map)) {
|
||||
throw new \InvalidArgumentException('map must be array', 1538327510);
|
||||
}
|
||||
|
||||
$this->configuration = $configuration;
|
||||
$this->default = $default;
|
||||
$this->index = $index;
|
||||
$this->map = array_map('strval', $map);
|
||||
}
|
||||
|
||||
public function getRoutePathRedecorationPattern(): string
|
||||
{
|
||||
return $this->buildRegularExpressionPattern(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function decorateForMatching(RouteCollection $collection, string $routePath): void
|
||||
{
|
||||
$decoratedRoutePath = null;
|
||||
$decoratedParameters = null;
|
||||
|
||||
$pattern = $this->buildRegularExpressionPattern();
|
||||
if (preg_match('#(?P<decoration>(?:' . $pattern . '))#', $routePath, $matches, PREG_UNMATCHED_AS_NULL)) {
|
||||
if (!isset($matches['decoration'])) {
|
||||
throw new \UnexpectedValueException(
|
||||
'Unexpected null value at end of URL',
|
||||
1538335671
|
||||
);
|
||||
}
|
||||
|
||||
$routePathValue = $matches['decoration'];
|
||||
$parameterValue = $matches['indexItems'] ?? $matches['slashedItems'] ?? $matches['regularItems'];
|
||||
$routePathValuePattern = $this->quoteForRegularExpressionPattern($routePathValue) . '$';
|
||||
$decoratedRoutePath = preg_replace('#' . $routePathValuePattern . '#', '', $routePath);
|
||||
|
||||
$mappedType = $this->map[$parameterValue] ?? null;
|
||||
if ($mappedType !== null) {
|
||||
$decoratedParameters = ['type' => $mappedType];
|
||||
} elseif ($this->default === $routePathValue) {
|
||||
$decoratedParameters = ['type' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($collection->all() as $route) {
|
||||
if ($decoratedRoutePath !== null) {
|
||||
$route->setOption(
|
||||
'_decoratedRoutePath',
|
||||
'/' . trim($decoratedRoutePath, '/')
|
||||
);
|
||||
}
|
||||
if ($decoratedParameters !== null) {
|
||||
$route->setOption(
|
||||
'_decoratedParameters',
|
||||
$decoratedParameters
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function decorateForGeneration(RouteCollection $collection, array $parameters): void
|
||||
{
|
||||
$type = isset($parameters['type']) ? (string)$parameters['type'] : null;
|
||||
$value = $this->resolveValue($type);
|
||||
// If the type is > 0 but the value could not be resolved,
|
||||
// the type is appended as GET argument, which can be resolved already anyway.
|
||||
// This happens when the PageTypeDecorator is used, but hasn't been configured for all available types.
|
||||
if (!empty($type) && ($value === '' || $value === $this->default)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$considerIndex = $value !== '' && in_array($value[0], static::ROUTE_PATH_DELIMITERS);
|
||||
if ($value !== '' && !in_array($value[0], static::ROUTE_PATH_DELIMITERS)) {
|
||||
$value = '/' . $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var Route $existingRoute
|
||||
*/
|
||||
foreach ($collection->all() as $existingRoute) {
|
||||
$existingRoutePath = rtrim($existingRoute->getPath(), '/');
|
||||
if ($considerIndex && $existingRoutePath === '') {
|
||||
$existingRoutePath = $this->index;
|
||||
}
|
||||
$existingRoute->setPath($existingRoutePath . $value);
|
||||
$deflatedParameters = $existingRoute->getOption('deflatedParameters') ?? $parameters;
|
||||
if (isset($deflatedParameters['type'])) {
|
||||
unset($deflatedParameters['type']);
|
||||
$existingRoute->setOption(
|
||||
'deflatedParameters',
|
||||
$deflatedParameters
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value exists inside the map.
|
||||
*/
|
||||
protected function resolveValue(?string $type): string
|
||||
{
|
||||
$index = array_search($type, $this->map, true);
|
||||
if ($index !== false) {
|
||||
return $index;
|
||||
}
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a regexp out of the map.
|
||||
*/
|
||||
protected function buildRegularExpressionPattern(bool $useNames = true): string
|
||||
{
|
||||
$items = array_keys($this->map);
|
||||
if ($this->default !== '' && !in_array($this->default, $items, true)) {
|
||||
$items[] = $this->default;
|
||||
}
|
||||
$slashedItems = array_filter($items, [$this, 'needsSlashPrefix']);
|
||||
$regularItems = array_diff($items, $slashedItems);
|
||||
|
||||
$slashedItems = array_map([$this, 'quoteForRegularExpressionPattern'], $slashedItems);
|
||||
$regularItems = array_map([$this, 'quoteForRegularExpressionPattern'], $regularItems);
|
||||
|
||||
$patterns = [];
|
||||
if (!empty($slashedItems)) {
|
||||
$name = $useNames ? '?P<slashedItems>' : '';
|
||||
$patterns[] = '(?:^|/)(' . $name . implode('|', $slashedItems) . ')';
|
||||
}
|
||||
if (!empty($regularItems) && !empty($this->index)) {
|
||||
$name = $useNames ? '?P<indexItems>' : '';
|
||||
$indexPattern = $this->quoteForRegularExpressionPattern($this->index);
|
||||
$patterns[] = '^' . $indexPattern . '(' . $name . '(?:' . implode('|', $regularItems) . '))';
|
||||
}
|
||||
if (!empty($regularItems)) {
|
||||
$name = $useNames ? '?P<regularItems>' : '';
|
||||
$patterns[] = '(' . $name . implode('|', $regularItems) . ')';
|
||||
}
|
||||
return '(?:' . implode('|', $patterns) . ')$';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for regexps.
|
||||
*/
|
||||
protected function quoteForRegularExpressionPattern(string $value): string
|
||||
{
|
||||
return preg_quote($value, '#');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a slash should be prefixed.
|
||||
*/
|
||||
protected function needsSlashPrefix(string $value): bool
|
||||
{
|
||||
return !in_array(
|
||||
$value[0] ?? '',
|
||||
static::ROUTE_PATH_DELIMITERS,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface;
|
||||
use TYPO3\CMS\Core\Routing\PageArguments;
|
||||
use TYPO3\CMS\Core\Routing\Route;
|
||||
use TYPO3\CMS\Core\Routing\RouteCollection;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
|
||||
/**
|
||||
* Used for plugins like EXT:felogin.
|
||||
*
|
||||
* This is usually used for arguments that are built with a `tx_myplugin_pi1` as namespace in GET / POST parameter.
|
||||
*
|
||||
* routeEnhancers:
|
||||
* ForgotPassword:
|
||||
* type: Plugin
|
||||
* routePath: '/forgot-pw/{user_id}/{hash}/'
|
||||
* namespace: 'tx_felogin_pi1'
|
||||
* _arguments:
|
||||
* user_id: uid
|
||||
* requirements:
|
||||
* user_id: '[a-z]+'
|
||||
* hash: '[a-z]{0-6}'
|
||||
*/
|
||||
class PluginEnhancer extends AbstractEnhancer implements RoutingEnhancerInterface, InflatableEnhancerInterface, ResultingInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace;
|
||||
|
||||
public function __construct(array $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
$this->namespace = $this->configuration['namespace'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildResult(Route $route, array $results, array $remainingQueryParameters = []): PageArguments
|
||||
{
|
||||
$variableProcessor = $this->getVariableProcessor();
|
||||
// determine those parameters that have been processed
|
||||
$parameters = array_intersect_key(
|
||||
$results,
|
||||
array_flip($route->compile()->getPathVariables())
|
||||
);
|
||||
// strip of those that where not processed (internals like _route, etc.)
|
||||
$internals = array_diff_key($results, $parameters);
|
||||
$matchedVariableNames = array_keys($parameters);
|
||||
|
||||
$staticMappers = $route->filterAspects([StaticMappableAspectInterface::class], $matchedVariableNames);
|
||||
$dynamicCandidates = array_diff_key($parameters, $staticMappers, $route->getOption('_static') ?? []);
|
||||
|
||||
// all route arguments
|
||||
$routeArguments = $this->inflateParameters($parameters, $internals);
|
||||
// dynamic arguments, that don't have a static mapper
|
||||
$dynamicArguments = $variableProcessor
|
||||
->inflateNamespaceParameters($dynamicCandidates, $this->namespace);
|
||||
// route arguments, that don't appear in dynamic arguments
|
||||
$staticArguments = ArrayUtility::arrayDiffKeyRecursive($routeArguments, $dynamicArguments);
|
||||
|
||||
$page = $route->getOption('_page');
|
||||
$pageId = (int)(isset($page['t3ver_oid']) && $page['t3ver_oid'] > 0 ? $page['t3ver_oid'] : $page['uid']);
|
||||
$pageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $pageId);
|
||||
// See PageSlugCandidateProvider where this is added.
|
||||
if ($page['MPvar'] ?? '') {
|
||||
$routeArguments['MP'] = $page['MPvar'];
|
||||
}
|
||||
$type = $this->resolveType($route, $remainingQueryParameters);
|
||||
return new PageArguments($pageId, $type, $routeArguments, $staticArguments, $remainingQueryParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enhanceForMatching(RouteCollection $collection): void
|
||||
{
|
||||
/** @var Route $defaultPageRoute */
|
||||
$defaultPageRoute = $collection->get('default');
|
||||
$variant = $this->getVariant($defaultPageRoute, $this->configuration);
|
||||
$collection->add('enhancer_' . $this->namespace . spl_object_hash($variant), $variant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a variant of a route based on the given configuration.
|
||||
*/
|
||||
protected function getVariant(Route $defaultPageRoute, array $configuration): Route
|
||||
{
|
||||
$arguments = $configuration['_arguments'] ?? [];
|
||||
unset($configuration['_arguments']);
|
||||
|
||||
$variableProcessor = $this->getVariableProcessor();
|
||||
$routePath = $this->modifyRoutePath($configuration['routePath']);
|
||||
$routePath = $variableProcessor->deflateRoutePath($routePath, $this->namespace, $arguments);
|
||||
$variant = clone $defaultPageRoute;
|
||||
$variant->setPath(rtrim($variant->getPath(), '/') . '/' . ltrim($routePath, '/'));
|
||||
$variant->addOptions(['_enhancer' => $this, '_arguments' => $arguments]);
|
||||
$defaults = $variableProcessor->deflateKeys($this->configuration['defaults'] ?? [], $this->namespace, $arguments);
|
||||
// only keep `defaults` that are actually used in `routePath`
|
||||
$variant->setDefaults($this->filterValuesByPathVariables($variant, $defaults));
|
||||
$this->applyRouteAspects($variant, $this->aspects, $this->namespace);
|
||||
$this->applyRequirements($variant, $this->configuration['requirements'] ?? [], $this->namespace);
|
||||
$this->applyStaticVariables($variant, $this->configuration['static'] ?? [], $this->namespace);
|
||||
return $variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enhanceForGeneration(RouteCollection $collection, array $parameters): void
|
||||
{
|
||||
// No parameter for this namespace given, so this route does not fit the requirements
|
||||
if (!is_array($parameters[$this->namespace] ?? null)) {
|
||||
return;
|
||||
}
|
||||
/** @var Route $defaultPageRoute */
|
||||
$defaultPageRoute = $collection->get('default');
|
||||
$variant = $this->getVariant($defaultPageRoute, $this->configuration);
|
||||
$compiledRoute = $variant->compile();
|
||||
// contains all given parameters, even if not used as variables in route
|
||||
$deflatedParameters = $this->deflateParameters($variant, $parameters);
|
||||
$variables = array_flip($compiledRoute->getPathVariables());
|
||||
$mergedParams = array_replace($variant->getDefaults(), $deflatedParameters);
|
||||
// all params must be given, otherwise we exclude this variant
|
||||
if ($variables === [] || array_diff_key($variables, $mergedParams) !== []) {
|
||||
return;
|
||||
}
|
||||
$variant->addOptions(['deflatedParameters' => $deflatedParameters]);
|
||||
$collection->add('enhancer_' . $this->namespace . spl_object_hash($variant), $variant);
|
||||
}
|
||||
|
||||
protected function deflateParameters(Route $route, array $parameters): array
|
||||
{
|
||||
return $this->getVariableProcessor()->deflateNamespaceParameters(
|
||||
$parameters,
|
||||
$this->namespace,
|
||||
$route->getArguments()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $parameters Actual parameter payload to be used
|
||||
* @param array $internals Internal instructions (_route, _controller, ...)
|
||||
*/
|
||||
public function inflateParameters(array $parameters, array $internals = []): array
|
||||
{
|
||||
return $this->getVariableProcessor()
|
||||
->inflateNamespaceParameters($parameters, $this->namespace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Routing\PageArguments;
|
||||
use TYPO3\CMS\Core\Routing\Route;
|
||||
|
||||
/**
|
||||
* Extend the Resulting Interface to explain that this route builds the page arguments itself, instead of having
|
||||
* the PageRouter having to deal with that.
|
||||
*/
|
||||
interface ResultingInterface
|
||||
{
|
||||
public function buildResult(Route $route, array $results, array $remainingQueryParameters = []): PageArguments;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Interface for enhancers
|
||||
*/
|
||||
interface RoutingEnhancerInterface extends EnhancerInterface
|
||||
{
|
||||
/**
|
||||
* Extends route collection with all routes. Used during URL resolving.
|
||||
*/
|
||||
public function enhanceForMatching(RouteCollection $collection): void;
|
||||
|
||||
/**
|
||||
* Extends route collection with routes that are relevant for given
|
||||
* parameters. Used during URL generation.
|
||||
*/
|
||||
public function enhanceForGeneration(RouteCollection $collection, array $parameters): void;
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface;
|
||||
use TYPO3\CMS\Core\Routing\PageArguments;
|
||||
use TYPO3\CMS\Core\Routing\Route;
|
||||
use TYPO3\CMS\Core\Routing\RouteCollection;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
|
||||
/**
|
||||
* This is usually used for simple GET arguments that have no namespace (e.g. not plugins).
|
||||
*
|
||||
* routeEnhancers
|
||||
* Categories:
|
||||
* type: Simple
|
||||
* routePath: '/cmd/{category_id}/{scope_id}'
|
||||
* _arguments:
|
||||
* category_id: 'category/id'
|
||||
* scope_id: 'scope/id'
|
||||
*/
|
||||
class SimpleEnhancer extends AbstractEnhancer implements RoutingEnhancerInterface, InflatableEnhancerInterface, ResultingInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(array $configuration)
|
||||
{
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildResult(Route $route, array $results, array $remainingQueryParameters = []): PageArguments
|
||||
{
|
||||
// determine those parameters that have been processed
|
||||
$parameters = array_intersect_key(
|
||||
$results,
|
||||
array_flip($route->compile()->getPathVariables())
|
||||
);
|
||||
// strip of those that where not processed (internals like _route, etc.)
|
||||
$internals = array_diff_key($results, $parameters);
|
||||
$matchedVariableNames = array_keys($parameters);
|
||||
|
||||
$staticMappers = $route->filterAspects([StaticMappableAspectInterface::class], $matchedVariableNames);
|
||||
$dynamicCandidates = array_diff_key($parameters, $staticMappers, $route->getOption('_static') ?? []);
|
||||
|
||||
// all route arguments
|
||||
$routeArguments = $this->inflateParameters($parameters, $internals);
|
||||
// dynamic arguments, that don't have a static mapper
|
||||
$dynamicArguments = $this->inflateParameters($dynamicCandidates);
|
||||
// route arguments, that don't appear in dynamic arguments
|
||||
$staticArguments = ArrayUtility::arrayDiffKeyRecursive($routeArguments, $dynamicArguments);
|
||||
|
||||
$page = $route->getOption('_page');
|
||||
$pageId = (int)(isset($page['t3ver_oid']) && $page['t3ver_oid'] > 0 ? $page['t3ver_oid'] : $page['uid']);
|
||||
$pageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $pageId);
|
||||
// See PageSlugCandidateProvider where this is added.
|
||||
if ($page['MPvar'] ?? '') {
|
||||
$routeArguments['MP'] = $page['MPvar'];
|
||||
}
|
||||
$type = $this->resolveType($route, $remainingQueryParameters);
|
||||
return new PageArguments($pageId, $type, $routeArguments, $staticArguments, $remainingQueryParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enhanceForMatching(RouteCollection $collection): void
|
||||
{
|
||||
/** @var Route $defaultPageRoute */
|
||||
$defaultPageRoute = $collection->get('default');
|
||||
$variant = $this->getVariant($defaultPageRoute, $this->configuration);
|
||||
$collection->add('enhancer_' . spl_object_hash($variant), $variant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a variant of a route based on the given configuration.
|
||||
*/
|
||||
protected function getVariant(Route $defaultPageRoute, array $configuration): Route
|
||||
{
|
||||
$arguments = $configuration['_arguments'] ?? [];
|
||||
unset($configuration['_arguments']);
|
||||
|
||||
$variableProcessor = $this->getVariableProcessor();
|
||||
$routePath = $this->modifyRoutePath($configuration['routePath']);
|
||||
$routePath = $variableProcessor->deflateRoutePath($routePath, null, $arguments);
|
||||
$variant = clone $defaultPageRoute;
|
||||
$variant->setPath(rtrim($variant->getPath(), '/') . '/' . ltrim($routePath, '/'));
|
||||
$variant->addOptions(['_enhancer' => $this, '_arguments' => $arguments]);
|
||||
$defaults = $variableProcessor->deflateKeys($this->configuration['defaults'] ?? [], null, $arguments);
|
||||
// only keep `defaults` that are actually used in `routePath`
|
||||
$variant->setDefaults($this->filterValuesByPathVariables($variant, $defaults));
|
||||
$this->applyRouteAspects($variant, $this->aspects);
|
||||
$this->applyRequirements($variant, $this->configuration['requirements'] ?? []);
|
||||
$this->applyStaticVariables($variant, $this->configuration['static'] ?? []);
|
||||
return $variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enhanceForGeneration(RouteCollection $collection, array $parameters): void
|
||||
{
|
||||
/** @var Route $defaultPageRoute */
|
||||
$defaultPageRoute = $collection->get('default');
|
||||
$variant = $this->getVariant($defaultPageRoute, $this->configuration);
|
||||
$compiledRoute = $variant->compile();
|
||||
// contains all given parameters, even if not used as variables in route
|
||||
$deflatedParameters = $this->getVariableProcessor()->deflateParameters($parameters, $variant->getArguments());
|
||||
$variables = array_flip($compiledRoute->getPathVariables());
|
||||
$mergedParams = array_replace($variant->getDefaults(), $deflatedParameters);
|
||||
// all params must be given, otherwise we exclude this variant
|
||||
if ($variables === [] || array_diff_key($variables, $mergedParams) !== []) {
|
||||
return;
|
||||
}
|
||||
$variant->addOptions(['deflatedParameters' => $deflatedParameters]);
|
||||
$collection->add('enhancer_' . spl_object_hash($variant), $variant);
|
||||
}
|
||||
|
||||
public function inflateParameters(array $parameters, array $internals = []): array
|
||||
{
|
||||
return $this->getVariableProcessor()->inflateParameters($parameters, $internals);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
<?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;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
|
||||
/**
|
||||
* Helper for processing various variables within a Route Enhancer
|
||||
*/
|
||||
#[Autoconfigure(public: true, shared: false)]
|
||||
class VariableProcessor
|
||||
{
|
||||
protected const LEVEL_DELIMITER = '___';
|
||||
protected const ARGUMENT_SEPARATOR = '/';
|
||||
protected const VARIABLE_PATTERN = '#\{(?P<modifier>!)?(?P<name>[^}]+)\}#';
|
||||
protected array $hashes = [];
|
||||
protected array $nestedValues = [];
|
||||
|
||||
public function __construct(private readonly VariableProcessorCache $cache) {}
|
||||
|
||||
protected function addHash(string $value): string
|
||||
{
|
||||
if (!$this->requiresHashing($value)) {
|
||||
return $value;
|
||||
}
|
||||
// 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(
|
||||
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
|
||||
['o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x'],
|
||||
$hash[0]
|
||||
);
|
||||
}
|
||||
$this->cache->hashes[$value] = $hash;
|
||||
}
|
||||
return $this->cache->hashes[$value];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \OutOfRangeException
|
||||
*/
|
||||
protected function resolveHash(string $hash): string
|
||||
{
|
||||
if (strlen($hash) < 31) {
|
||||
return $hash;
|
||||
}
|
||||
if (!isset($this->hashes[$hash])) {
|
||||
throw new \OutOfRangeException(
|
||||
'Hash not resolvable',
|
||||
1537633463
|
||||
);
|
||||
}
|
||||
return $this->hashes[$hash];
|
||||
}
|
||||
|
||||
protected function addNestedValue(string $value): string
|
||||
{
|
||||
if (!str_contains($value, static::ARGUMENT_SEPARATOR)) {
|
||||
return $value;
|
||||
}
|
||||
$nestedValue = str_replace(
|
||||
static::ARGUMENT_SEPARATOR,
|
||||
static::LEVEL_DELIMITER,
|
||||
$value
|
||||
);
|
||||
$this->nestedValues[$nestedValue] = $value;
|
||||
return $nestedValue;
|
||||
}
|
||||
|
||||
protected function resolveNestedValue(string $value): string
|
||||
{
|
||||
if (!str_contains($value, static::LEVEL_DELIMITER)) {
|
||||
return $value;
|
||||
}
|
||||
return $this->nestedValues[$value] ?? $value;
|
||||
}
|
||||
|
||||
public function deflateRoutePath(string $routePath, ?string $namespace = null, array $arguments = []): string
|
||||
{
|
||||
if (!preg_match_all(static::VARIABLE_PATTERN, $routePath, $matches)) {
|
||||
return $routePath;
|
||||
}
|
||||
|
||||
$replace = [];
|
||||
$search = $matches[0];
|
||||
$deflatedNames = $this->deflateValues($matches['name'], $namespace, $arguments);
|
||||
foreach ($deflatedNames as $index => $deflatedName) {
|
||||
$modifier = $matches['modifier'][$index] ?? '';
|
||||
$replace[] = '{' . $modifier . $deflatedName . '}';
|
||||
}
|
||||
return str_replace($search, $replace, $routePath);
|
||||
}
|
||||
|
||||
public function inflateRoutePath(string $routePath, ?string $namespace = null, array $arguments = []): string
|
||||
{
|
||||
if (!preg_match_all(static::VARIABLE_PATTERN, $routePath, $matches)) {
|
||||
return $routePath;
|
||||
}
|
||||
|
||||
$replace = [];
|
||||
$search = $matches[0];
|
||||
$inflatedNames = $this->inflateValues($matches['name'], $namespace, $arguments);
|
||||
foreach ($inflatedNames as $index => $inflatedName) {
|
||||
$modifier = $matches['modifier'][$index] ?? '';
|
||||
$replace[] = '{' . $modifier . $inflatedName . '}';
|
||||
}
|
||||
return str_replace($search, $replace, $routePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deflates (flattens) route/request parameters for a given namespace.
|
||||
*/
|
||||
public function deflateNamespaceParameters(array $parameters, string $namespace, array $arguments = []): array
|
||||
{
|
||||
if (empty($namespace) || empty($parameters[$namespace])) {
|
||||
return $parameters;
|
||||
}
|
||||
// prefix items of namespace parameters and apply argument mapping
|
||||
$namespaceParameters = $this->deflateKeys($parameters[$namespace], $namespace, $arguments, false);
|
||||
// deflate those array items
|
||||
$namespaceParameters = $this->deflateArray($namespaceParameters);
|
||||
unset($parameters[$namespace]);
|
||||
// merge with remaining array items
|
||||
return array_merge($parameters, $namespaceParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates (unflattens) route/request parameters.
|
||||
*/
|
||||
public function inflateNamespaceParameters(array $parameters, string $namespace, array $arguments = []): array
|
||||
{
|
||||
if (empty($namespace) || empty($parameters)) {
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
$parameters = $this->inflateArray($parameters, $namespace, $arguments);
|
||||
// apply argument mapping on items of inflated namespace parameters
|
||||
if (!empty($parameters[$namespace]) && !empty($arguments)) {
|
||||
$parameters[$namespace] = $this->inflateKeys($parameters[$namespace], null, $arguments, false);
|
||||
}
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deflates (flattens) route/request parameters for a given namespace.
|
||||
*/
|
||||
public function deflateParameters(array $parameters, array $arguments = []): array
|
||||
{
|
||||
$parameters = $this->deflateKeys($parameters, null, $arguments, false);
|
||||
return $this->deflateArray($parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates (unflattens) route/request parameters.
|
||||
*/
|
||||
public function inflateParameters(array $parameters, array $arguments = []): array
|
||||
{
|
||||
$parameters = $this->inflateArray($parameters, null, $arguments);
|
||||
return $this->inflateKeys($parameters, null, $arguments, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deflates keys names on the first level, now recursion into sub-arrays.
|
||||
* Can be used to adjust key names of route requirements, mappers, etc.
|
||||
*/
|
||||
public function deflateKeys(array $items, ?string $namespace = null, array $arguments = [], bool $hash = true): array
|
||||
{
|
||||
if (empty($items) || empty($arguments) && empty($namespace)) {
|
||||
return $items;
|
||||
}
|
||||
$keys = $this->deflateValues(array_keys($items), $namespace, $arguments, $hash);
|
||||
return array_combine(
|
||||
$keys,
|
||||
array_values($items)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates keys names on the first level, now recursion into sub-arrays.
|
||||
* Can be used to adjust key names of route requirements, mappers, etc.
|
||||
*/
|
||||
public function inflateKeys(array $items, ?string $namespace = null, array $arguments = [], bool $hash = true): array
|
||||
{
|
||||
if (empty($items) || empty($arguments) && empty($namespace)) {
|
||||
return $items;
|
||||
}
|
||||
$keys = $this->inflateValues(array_keys($items), $namespace, $arguments, $hash);
|
||||
return array_combine(
|
||||
$keys,
|
||||
array_values($items)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deflates plain values.
|
||||
*/
|
||||
protected function deflateValues(array $values, ?string $namespace = null, array $arguments = [], bool $hash = true): array
|
||||
{
|
||||
if (empty($values) || empty($arguments) && empty($namespace)) {
|
||||
return $values;
|
||||
}
|
||||
$namespacePrefix = $namespace ? $namespace . static::LEVEL_DELIMITER : '';
|
||||
$arguments = array_map('strval', $arguments);
|
||||
return array_map(
|
||||
function (string $value) use ($arguments, $namespacePrefix, $hash) {
|
||||
$value = $arguments[$value] ?? $value;
|
||||
$value = $this->addNestedValue($value);
|
||||
$value = $namespacePrefix . $value;
|
||||
if (!$hash) {
|
||||
return $value;
|
||||
}
|
||||
return $this->addHash($value);
|
||||
},
|
||||
$values
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates plain values.
|
||||
*/
|
||||
protected function inflateValues(array $values, ?string $namespace = null, array $arguments = [], bool $hash = true): array
|
||||
{
|
||||
if (empty($values) || empty($arguments) && empty($namespace)) {
|
||||
return $values;
|
||||
}
|
||||
$arguments = array_map('strval', $arguments);
|
||||
$namespacePrefix = $namespace ? $namespace . static::LEVEL_DELIMITER : '';
|
||||
return array_map(
|
||||
function (string $value) use ($arguments, $namespacePrefix, $hash) {
|
||||
if ($hash) {
|
||||
$value = $this->resolveHash($value);
|
||||
}
|
||||
if (!empty($namespacePrefix) && str_starts_with($value, $namespacePrefix)) {
|
||||
$value = substr($value, strlen($namespacePrefix));
|
||||
}
|
||||
$value = $this->resolveNestedValue($value);
|
||||
$index = array_search($value, $arguments, true);
|
||||
return $index !== false ? $index : $value;
|
||||
},
|
||||
$values
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deflates (flattens) array having nested structures.
|
||||
*/
|
||||
protected function deflateArray(array $array, string $prefix = ''): array
|
||||
{
|
||||
$delimiter = static::LEVEL_DELIMITER;
|
||||
if ($prefix !== '' && !str_ends_with($prefix, $delimiter)) {
|
||||
$prefix .= static::LEVEL_DELIMITER;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($array as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$result = array_replace(
|
||||
$result,
|
||||
$this->deflateArray(
|
||||
$value,
|
||||
$prefix . $key . static::LEVEL_DELIMITER
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$deflatedKey = $this->addHash($prefix . $key);
|
||||
$result[$deflatedKey] = $value;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflates (unflattens) an array into nested structures.
|
||||
*
|
||||
* @param string $namespace
|
||||
*/
|
||||
protected function inflateArray(array $array, ?string $namespace, array $arguments): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($array as $key => $value) {
|
||||
$inflatedKey = $this->resolveHash((string)$key);
|
||||
// inflate nested values `namespace__any__nested` -> `namespace__any/nested`
|
||||
$inflatedKey = $this->inflateNestedValue($inflatedKey, $namespace, $arguments);
|
||||
$steps = explode(static::LEVEL_DELIMITER, $inflatedKey);
|
||||
$pointer = &$result;
|
||||
foreach ($steps as $step) {
|
||||
$pointer = &$pointer[$step];
|
||||
}
|
||||
$pointer = $value;
|
||||
unset($pointer);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function inflateNestedValue(string $value, ?string $namespace, array $arguments): string
|
||||
{
|
||||
$namespacePrefix = $namespace ? $namespace . static::LEVEL_DELIMITER : '';
|
||||
if (!empty($namespace) && !str_starts_with($value, $namespacePrefix)) {
|
||||
return $value;
|
||||
}
|
||||
$arguments = array_map('strval', $arguments);
|
||||
$possibleNestedValueKey = substr($value, strlen($namespacePrefix));
|
||||
$possibleNestedValue = $this->nestedValues[$possibleNestedValueKey] ?? null;
|
||||
if ($possibleNestedValue === null || !in_array($possibleNestedValue, $arguments, true)) {
|
||||
return $value;
|
||||
}
|
||||
return $namespacePrefix . $possibleNestedValue;
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
}
|
||||
Reference in New Issue
Block a user