1d539fcaf3e8f13a7c39cc37728869ce12a6adc6
[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 * map:
33 * '.html': 1
34 * 'menu.json': 13
35 */
36 class PageTypeDecorator extends AbstractEnhancer implements DecoratingEnhancerInterface
37 {
38 protected const PREFIXES = ['.', '-', '_'];
39
40 /**
41 * @var array
42 */
43 protected $configuration;
44
45 /**
46 * @var string
47 */
48 protected $default;
49
50 /**
51 * @var array
52 */
53 protected $map;
54
55 /**
56 * @param array $configuration
57 */
58 public function __construct(array $configuration)
59 {
60 $default = $configuration['default'] ?? '';
61 $map = $configuration['map'] ?? null;
62
63 if (!is_string($default)) {
64 throw new \InvalidArgumentException('default must be string', 1538327508);
65 }
66 if (!is_array($map)) {
67 throw new \InvalidArgumentException('map must be array', 1538327509);
68 }
69
70 $this->configuration = $configuration;
71 $this->default = $default;
72 $this->map = array_map('strval', $map);
73 }
74
75 /**
76 * {@inheritdoc}
77 */
78 public function decorateForMatching(RouteCollection $collection, array &$parameters, string &$routePath): void
79 {
80 $pattern = $this->buildRegularExpressionPattern();
81 if (!preg_match('#' . $pattern . '#', $routePath, $matches, PREG_UNMATCHED_AS_NULL)) {
82 $parameters['type'] = 0;
83 return;
84 }
85
86 $value = $matches['slashedItems'] ?? $matches['regularItems'] ?? null;
87 if (!is_string($value)) {
88 throw new \UnexpectedValueException(
89 'Unexpected null value at end of URL',
90 1538335671
91 );
92 }
93
94 $parameters['type'] = $this->map[$value] ?? 0;
95 $valuePattern = $this->quoteForRegularExpressionPattern($value) . '$';
96 $routePath = preg_replace('#' . $valuePattern . '#', '', $routePath);
97 }
98
99 /**
100 * {@inheritdoc}
101 */
102 public function decorateForGeneration(RouteCollection $collection, array &$parameters): void
103 {
104 $type = isset($parameters['type']) ? (string)$parameters['type'] : null;
105 $value = $this->resolveValue($type);
106 unset($parameters['type']);
107
108 if ($value !== '' && !in_array($value{0}, static::PREFIXES)) {
109 $value = '/' . $value;
110 }
111
112 /**
113 * @var string $routeName
114 * @var Route $existingRoute
115 */
116 foreach ($collection->all() as $routeName => $existingRoute) {
117 $existingRoute->setPath(rtrim($existingRoute->getPath(), '/') . $value);
118 $deflatedParameters = $existingRoute->getOption('deflatedParameters');
119 if (isset($deflatedParameters['type'])) {
120 unset($deflatedParameters['type']);
121 $existingRoute->setOption(
122 'deflatedParameters',
123 $deflatedParameters
124 );
125 }
126 }
127 }
128
129 /**
130 * Checks if the value exists inside the map.
131 *
132 * @param string|null $type
133 * @return string
134 */
135 protected function resolveValue(?string $type): string
136 {
137 $index = array_search($type, $this->map, true);
138 if ($index !== false) {
139 return $index;
140 }
141 return $this->default;
142 }
143
144 /**
145 * Builds a regexp out of the map.
146 * @return string
147 */
148 protected function buildRegularExpressionPattern(): string
149 {
150 $items = array_keys($this->map);
151 $slashedItems = array_filter($items, [$this, 'needsSlashPrefix']);
152 $regularItems = array_diff($items, $slashedItems);
153
154 $slashedItems = array_map([$this, 'quoteForRegularExpressionPattern'], $slashedItems);
155 $regularItems = array_map([$this, 'quoteForRegularExpressionPattern'], $regularItems);
156
157 $patterns = [];
158 if (!empty($slashedItems)) {
159 $patterns[] = '/(?P<slashedItems>' . implode('|', $slashedItems) . ')';
160 }
161 if (!empty($regularItems)) {
162 $patterns[] = '(?P<regularItems>' . implode('|', $regularItems) . ')';
163 }
164 return '(?:' . implode('|', $patterns) . ')$';
165 }
166
167 /**
168 * Helper method for regexps.
169 *
170 * @param string $value
171 * @return string
172 */
173 protected function quoteForRegularExpressionPattern(string $value): string
174 {
175 return preg_quote($value, '#');
176 }
177
178 /**
179 * Checks if a slash should be prefixed.
180 *
181 * @param string $value
182 * @return bool
183 */
184 protected function needsSlashPrefix(string $value): bool
185 {
186 return !in_array(
187 $value{0} ?? '',
188 static::PREFIXES,
189 true
190 );
191 }
192 }