[BUGFIX] Resolve slashed values in PageTypeDecorator correctly
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Routing / Enhancer / PageTypeDecorator.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Core\Routing\Enhancer;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use TYPO3\CMS\Core\Routing\Route;
20 use TYPO3\CMS\Core\Routing\RouteCollection;
21
22 /**
23 * Resolves a static list (like page.typeNum) against a file pattern. Usually added on the very last part
24 * of the URL.
25 * It is important that the PageType Enhancer is executed at the very end in your configuration, as it modifies
26 * EXISTING route variants.
27 *
28 * routeEnhancers:
29 * PageTypeSuffix:
30 * type: PageType
31 * default: ''
32 * index: 'index'
33 * map:
34 * '.html': 1
35 * 'menu.json': 13
36 */
37 class PageTypeDecorator extends AbstractEnhancer implements DecoratingEnhancerInterface
38 {
39 protected const ROUTE_PATH_DELIMITERS = ['.', '-', '_'];
40
41 /**
42 * @var array
43 */
44 protected $configuration;
45
46 /**
47 * @var string
48 */
49 protected $default;
50
51 /**
52 * @var string
53 */
54 protected $index;
55
56 /**
57 * @var array
58 */
59 protected $map;
60
61 /**
62 * @param array $configuration
63 */
64 public function __construct(array $configuration)
65 {
66 $default = $configuration['default'] ?? '';
67 $index = $configuration['index'] ?? 'index';
68 $map = $configuration['map'] ?? null;
69
70 if (!is_string($default)) {
71 throw new \InvalidArgumentException('default must be string', 1538327508);
72 }
73 if (!is_string($index)) {
74 throw new \InvalidArgumentException('index must be string', 1538327509);
75 }
76 if (!is_array($map)) {
77 throw new \InvalidArgumentException('map must be array', 1538327510);
78 }
79
80 $this->configuration = $configuration;
81 $this->default = $default;
82 $this->index = $index;
83 $this->map = array_map('strval', $map);
84 }
85
86 /**
87 * @return string
88 */
89 public function getRoutePathRedecorationPattern(): string
90 {
91 return $this->buildRegularExpressionPattern(false);
92 }
93
94 /**
95 * {@inheritdoc}
96 */
97 public function decorateForMatching(RouteCollection $collection, string $routePath): void
98 {
99 $decoratedRoutePath = null;
100 $decoratedParameters = ['type' => 0];
101
102 $pattern = $this->buildRegularExpressionPattern();
103 if (preg_match('#(?P<decoration>(?:' . $pattern . '))#', $routePath, $matches, PREG_UNMATCHED_AS_NULL)) {
104 if (!isset($matches['decoration'])) {
105 throw new \UnexpectedValueException(
106 'Unexpected null value at end of URL',
107 1538335671
108 );
109 }
110
111 $routePathValue = $matches['decoration'];
112 $parameterValue = $matches['indexItems'] ?? $matches['slashedItems'] ?? $matches['regularItems'];
113 $routePathValuePattern = $this->quoteForRegularExpressionPattern($routePathValue) . '$';
114 $decoratedRoutePath = preg_replace('#' . $routePathValuePattern . '#', '', $routePath);
115 $decoratedParameters = ['type' => $this->map[$parameterValue] ?? 0];
116 }
117
118 foreach ($collection->all() as $route) {
119 if ($decoratedRoutePath !== null) {
120 $route->setOption(
121 '_decoratedRoutePath',
122 '/' . trim($decoratedRoutePath, '/')
123 );
124 }
125 $route->setOption('_decoratedParameters', $decoratedParameters);
126 }
127 }
128
129 /**
130 * {@inheritdoc}
131 */
132 public function decorateForGeneration(RouteCollection $collection, array $parameters): void
133 {
134 $type = isset($parameters['type']) ? (string)$parameters['type'] : null;
135 $value = $this->resolveValue($type);
136
137 $considerIndex = $value !== ''
138 && in_array($value{0}, static::ROUTE_PATH_DELIMITERS);
139 if ($value !== '' && !in_array($value{0}, static::ROUTE_PATH_DELIMITERS)) {
140 $value = '/' . $value;
141 }
142
143 /**
144 * @var string $routeName
145 * @var Route $existingRoute
146 */
147 foreach ($collection->all() as $routeName => $existingRoute) {
148 $existingRoutePath = rtrim($existingRoute->getPath(), '/');
149 if ($considerIndex && $existingRoutePath === '') {
150 $existingRoutePath = $this->index;
151 }
152 $existingRoute->setPath($existingRoutePath . $value);
153 $deflatedParameters = $existingRoute->getOption('deflatedParameters') ?? $parameters;
154 if (isset($deflatedParameters['type'])) {
155 unset($deflatedParameters['type']);
156 $existingRoute->setOption(
157 'deflatedParameters',
158 $deflatedParameters
159 );
160 }
161 }
162 }
163
164 /**
165 * Checks if the value exists inside the map.
166 *
167 * @param string|null $type
168 * @return string
169 */
170 protected function resolveValue(?string $type): string
171 {
172 $index = array_search($type, $this->map, true);
173 if ($index !== false) {
174 return $index;
175 }
176 return $this->default;
177 }
178
179 /**
180 * Builds a regexp out of the map.
181 *
182 * @param bool $useNames
183 * @return string
184 */
185 protected function buildRegularExpressionPattern(bool $useNames = true): string
186 {
187 $items = array_keys($this->map);
188 $slashedItems = array_filter($items, [$this, 'needsSlashPrefix']);
189 $regularItems = array_diff($items, $slashedItems);
190
191 $slashedItems = array_map([$this, 'quoteForRegularExpressionPattern'], $slashedItems);
192 $regularItems = array_map([$this, 'quoteForRegularExpressionPattern'], $regularItems);
193
194 $patterns = [];
195 if (!empty($slashedItems)) {
196 $name = $useNames ? '?P<slashedItems>' : '';
197 $patterns[] = '(?:^|/)(' . $name . implode('|', $slashedItems) . ')';
198 }
199 if (!empty($regularItems) && !empty($this->index)) {
200 $name = $useNames ? '?P<indexItems>' : '';
201 $indexPattern = $this->quoteForRegularExpressionPattern($this->index);
202 $patterns[] = '(' . $name . $indexPattern . '(?:' . implode('|', $regularItems) . '))';
203 }
204 if (!empty($regularItems)) {
205 $name = $useNames ? '?P<regularItems>' : '';
206 $patterns[] = '(' . $name . implode('|', $regularItems) . ')';
207 }
208 return '(?:' . implode('|', $patterns) . ')$';
209 }
210
211 /**
212 * Helper method for regexps.
213 *
214 * @param string $value
215 * @return string
216 */
217 protected function quoteForRegularExpressionPattern(string $value): string
218 {
219 return preg_quote($value, '#');
220 }
221
222 /**
223 * Checks if a slash should be prefixed.
224 *
225 * @param string $value
226 * @return bool
227 */
228 protected function needsSlashPrefix(string $value): bool
229 {
230 return !in_array(
231 $value{0} ?? '',
232 static::ROUTE_PATH_DELIMITERS,
233 true
234 );
235 }
236 }