[BUGFIX] Respect GET parameters when generating canonicalized URLs
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / DataProcessing / LanguageMenuProcessor.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Frontend\DataProcessing;
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\SiteMatcher;
20 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;
23 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
24 use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
25 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
26 use TYPO3\CMS\Frontend\Utility\CanonicalizationUtility;
27
28 /**
29 * This menu processor generates a json encoded menu string that will be
30 * decoded again and assigned to FLUIDTEMPLATE as variable.
31 *
32 * Options:
33 * if - TypoScript if condition
34 * languages - A list of languages id's (e.g. 0,1,2) to use for the menu
35 * creation or 'auto' to load from system or site languages
36 * as - The variable to be used within the result
37 *
38 * Example TypoScript configuration:
39 * 10 = TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor
40 * 10 {
41 * as = languagenavigation
42 * }
43 */
44 class LanguageMenuProcessor implements DataProcessorInterface
45 {
46 protected const LINK_PLACEHOLDER = '###LINKPLACEHOLDER###';
47
48 /**
49 * The content object renderer
50 *
51 * @var ContentObjectRenderer
52 */
53 public $cObj;
54
55 /**
56 * The processor configuration
57 *
58 * @var array
59 */
60 protected $processorConfiguration;
61
62 /**
63 * Allowed configuration keys for menu generation, other keys
64 * will throw an exception to prevent configuration errors.
65 *
66 * @var array
67 */
68 protected $allowedConfigurationKeys = [
69 'if',
70 'if.',
71 'languages',
72 'languages.',
73 'as',
74 'addQueryString',
75 'addQueryString.'
76 ];
77
78 /**
79 * Remove keys from configuration that should not be passed
80 * to HMENU to prevent configuration errors
81 *
82 * @var array
83 */
84 protected $removeConfigurationKeysForHmenu = [
85 'languages',
86 'languages.',
87 'as'
88 ];
89
90 /**
91 * @var array
92 */
93 protected $menuConfig = [
94 'special' => 'language',
95 'addQueryString' => 1,
96 'addQueryString.' => [
97 'method' => 'GET'
98 ],
99 'wrap' => '[|]'
100 ];
101
102 /**
103 * @var array
104 */
105 protected $menuLevelConfig = [
106 'doNotLinkIt' => '1',
107 'wrapItemAndSub' => '{|}, |*| {|}, |*| {|}',
108 'stdWrap.' => [
109 'cObject' => 'COA',
110 'cObject.' => [
111 '1' => 'LOAD_REGISTER',
112 '1.' => [
113 'languageId.' => [
114 'cObject' => 'TEXT',
115 'cObject.' => [
116 'value.' => [
117 'data' => 'register:languages_HMENU'
118 ],
119 'listNum.' => [
120 'stdWrap.' => [
121 'data' => 'register:count_HMENU_MENUOBJ',
122 'wrap' => '|-1'
123 ],
124 'splitChar' => ','
125 ]
126 ]
127 ]
128 ],
129 '10' => 'TEXT',
130 '10.' => [
131 'stdWrap.' => [
132 'data' => 'register:languageId'
133 ],
134 'wrap' => '"languageId":|'
135 ],
136 '11' => 'USER',
137 '11.' => [
138 'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
139 'language.' => [
140 'data' => 'register:languageId'
141 ],
142 'field' => 'locale',
143 'stdWrap.' => [
144 'wrap' => ',"locale":|'
145 ]
146 ],
147 '20' => 'USER',
148 '20.' => [
149 'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
150 'language.' => [
151 'data' => 'register:languageId'
152 ],
153 'field' => 'title',
154 'stdWrap.' => [
155 'wrap' => ',"title":|'
156 ]
157 ],
158 '21' => 'USER',
159 '21.' => [
160 'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
161 'language.' => [
162 'data' => 'register:languageId'
163 ],
164 'field' => 'navigationTitle',
165 'stdWrap.' => [
166 'wrap' => ',"navigationTitle":|'
167 ]
168 ],
169 '22' => 'USER',
170 '22.' => [
171 'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
172 'language.' => [
173 'data' => 'register:languageId'
174 ],
175 'field' => 'twoLetterIsoCode',
176 'stdWrap.' => [
177 'wrap' => ',"twoLetterIsoCode":|'
178 ]
179 ],
180 '23' => 'USER',
181 '23.' => [
182 'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
183 'language.' => [
184 'data' => 'register:languageId'
185 ],
186 'field' => 'hreflang',
187 'stdWrap.' => [
188 'wrap' => ',"hreflang":|'
189 ]
190 ],
191 '24' => 'USER',
192 '24.' => [
193 'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
194 'language.' => [
195 'data' => 'register:languageId'
196 ],
197 'field' => 'direction',
198 'stdWrap.' => [
199 'wrap' => ',"direction":|'
200 ]
201 ],
202 '90' => 'TEXT',
203 '90.' => [
204 'value' => self::LINK_PLACEHOLDER,
205 'wrap' => ',"link":|',
206 ],
207 '91' => 'TEXT',
208 '91.' => [
209 'value' => '0',
210 'wrap' => ',"active":|'
211 ],
212 '92' => 'TEXT',
213 '92.' => [
214 'value' => '0',
215 'wrap' => ',"current":|'
216 ],
217 '93' => 'TEXT',
218 '93.' => [
219 'value' => '1',
220 'wrap' => ',"available":|'
221 ],
222 '99' => 'RESTORE_REGISTER'
223 ]
224 ]
225 ];
226
227 /**
228 * @var array
229 */
230 protected $menuDefaults = [
231 'as' => 'languagemenu'
232 ];
233
234 /**
235 * @var string
236 */
237 protected $menuTargetVariableName;
238
239 /**
240 * @var ContentDataProcessor
241 */
242 protected $contentDataProcessor;
243
244 /**
245 * Constructor
246 */
247 public function __construct()
248 {
249 $this->contentDataProcessor = GeneralUtility::makeInstance(ContentDataProcessor::class);
250 }
251
252 /**
253 * Get configuration value from processorConfiguration
254 *
255 * @param string $key
256 * @return string
257 */
258 protected function getConfigurationValue(string $key): string
259 {
260 return $this->cObj->stdWrapValue($key, $this->processorConfiguration, $this->menuDefaults[$key]);
261 }
262
263 /**
264 * @return TypoScriptFrontendController
265 */
266 protected function getTypoScriptFrontendController(): TypoScriptFrontendController
267 {
268 return $GLOBALS['TSFE'];
269 }
270
271 /**
272 * Returns the currently configured "site" if a site is configured (= resolved) in the current request.
273 *
274 * @return SiteInterface
275 * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
276 */
277 protected function getCurrentSite(): SiteInterface
278 {
279 $matcher = GeneralUtility::makeInstance(SiteMatcher::class);
280 return $matcher->matchByPageId((int)$this->getTypoScriptFrontendController()->id);
281 }
282
283 /**
284 * JSON Encode
285 *
286 * @param mixed $value
287 * @return string
288 */
289 protected function jsonEncode($value): string
290 {
291 return json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE);
292 }
293
294 /**
295 * @throws \InvalidArgumentException
296 */
297 protected function validateConfiguration()
298 {
299 $invalidArguments = [];
300 foreach ($this->processorConfiguration as $key => $value) {
301 if (!in_array($key, $this->allowedConfigurationKeys)) {
302 $invalidArguments[str_replace('.', '', $key)] = $key;
303 }
304 }
305 if (!empty($invalidArguments)) {
306 throw new \InvalidArgumentException('LanguageMenuProcessor configuration contains invalid arguments: ' . implode(', ', $invalidArguments), 1522959188);
307 }
308 }
309
310 /**
311 * Process languages and filter the configuration
312 */
313 protected function prepareConfiguration(): void
314 {
315 $this->menuConfig += $this->processorConfiguration;
316
317 // Process languages
318 if (empty($this->menuConfig['languages']) && empty($this->menuConfig['languages.'])) {
319 $this->menuConfig['special.']['value'] = 'auto';
320 } elseif (!empty($this->menuConfig['languages.'])) {
321 $this->menuConfig['special.']['value'] = $this->cObj->stdWrap($this->menuConfig['languages'], $this->menuConfig['languages.']);
322 } else {
323 $this->menuConfig['special.']['value'] = $this->menuConfig['languages'];
324 }
325
326 // Filter configuration
327 foreach ($this->menuConfig as $key => $value) {
328 if (in_array($key, $this->removeConfigurationKeysForHmenu, true)) {
329 unset($this->menuConfig[$key]);
330 }
331 }
332
333 $paramsToExclude = CanonicalizationUtility::getParamsToExcludeForCanonicalizedUrl(
334 (int)$this->getTypoScriptFrontendController()->id,
335 (array)$GLOBALS['TYPO3_CONF_VARS']['FE']['additionalCanonicalizedUrlParameters']
336 );
337
338 $this->menuConfig['addQueryString.']['exclude'] = implode(
339 ',',
340 array_merge(
341 GeneralUtility::trimExplode(',', $this->menuConfig['addQueryString.']['exclude'] ?? '', true),
342 $paramsToExclude
343 )
344 );
345 }
346
347 /**
348 * Build the menu configuration so it can be treated by HMENU cObject
349 */
350 protected function buildConfiguration(): void
351 {
352 $this->menuConfig['1'] = 'TMENU';
353 $this->menuConfig['1.']['IProcFunc'] = LanguageMenuProcessor::class . '->replacePlaceholderInRenderedMenuItem';
354 $this->menuConfig['1.']['NO'] = '1';
355 $this->menuConfig['1.']['NO.'] = $this->menuLevelConfig;
356 $this->menuConfig['1.']['ACT'] = $this->menuConfig['1.']['NO'];
357 $this->menuConfig['1.']['ACT.'] = $this->menuConfig['1.']['NO.'];
358 $this->menuConfig['1.']['ACT.']['stdWrap.']['cObject.']['91.']['value'] = '1';
359 $this->menuConfig['1.']['CUR'] = $this->menuConfig['1.']['ACT'];
360 $this->menuConfig['1.']['CUR.'] = $this->menuConfig['1.']['ACT.'];
361 $this->menuConfig['1.']['CUR.']['stdWrap.']['cObject.']['92.']['value'] = '1';
362 $this->menuConfig['1.']['USERDEF1'] = $this->menuConfig['1.']['NO'];
363 $this->menuConfig['1.']['USERDEF1.'] = $this->menuConfig['1.']['NO.'];
364 $this->menuConfig['1.']['USERDEF1.']['stdWrap.']['cObject.']['93.']['value'] = '0';
365 $this->menuConfig['1.']['USERDEF2'] = $this->menuConfig['1.']['ACT'];
366 $this->menuConfig['1.']['USERDEF2.'] = $this->menuConfig['1.']['ACT.'];
367 $this->menuConfig['1.']['USERDEF2.']['stdWrap.']['cObject.']['93.']['value'] = '0';
368 }
369
370 /**
371 * Validate and Build the menu configuration so it can be treated by HMENU cObject
372 */
373 protected function validateAndBuildConfiguration(): void
374 {
375 // Validate Configuration
376 $this->validateConfiguration();
377
378 // Build Configuration
379 $this->prepareConfiguration();
380 $this->buildConfiguration();
381 }
382
383 /**
384 * @param ContentObjectRenderer $cObj The data of the content element or page
385 * @param array $contentObjectConfiguration The configuration of Content Object
386 * @param array $processorConfiguration The configuration of this processor
387 * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
388 * @return array the processed data as key/value store
389 */
390 public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData): array
391 {
392 $this->cObj = $cObj;
393 $this->processorConfiguration = $processorConfiguration;
394
395 // Get Configuration
396 $this->menuTargetVariableName = $this->getConfigurationValue('as');
397
398 // Validate and Build Configuration
399 $this->validateAndBuildConfiguration();
400
401 // Process Configuration
402 $menuContentObject = $cObj->getContentObject('HMENU');
403 $renderedMenu = $menuContentObject->render($this->menuConfig);
404 if ($renderedMenu) {
405 // Process menu
406 $menu = json_decode($renderedMenu, true);
407 $processedMenu = [];
408
409 foreach ($menu as $key => $language) {
410 $processedMenu[$key] = $language;
411 }
412
413 $processedData[$this->menuTargetVariableName] = $processedMenu;
414 }
415
416 return $processedData;
417 }
418
419 /**
420 * This UserFunc gets the link and the target
421 *
422 * @param array $menuItem
423 * @return array
424 */
425 public function replacePlaceholderInRenderedMenuItem(array $menuItem): array
426 {
427 $link = $this->jsonEncode($menuItem['linkHREF']['HREF']);
428
429 $menuItem['parts']['title'] = str_replace(self::LINK_PLACEHOLDER, $link, $menuItem['parts']['title']);
430
431 return $menuItem;
432 }
433
434 /**
435 * Returns the data from the field and language submitted by $conf in JSON format
436 *
437 * @param string Empty string (no content to process)
438 * @param array TypoScript configuration
439 * @return string JSON encoded data
440 * @throws \InvalidArgumentException
441 * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
442 */
443 public function getFieldAsJson(string $content, array $conf): string
444 {
445 // Support of stdWrap for parameters
446 if (isset($conf['language.'])) {
447 $conf['language'] = $this->cObj->stdWrap($conf['language'], $conf['language.']);
448 unset($conf['language.']);
449 }
450 if (isset($conf['field.'])) {
451 $conf['field'] = $this->cObj->stdWrap($conf['field'], $conf['field.']);
452 unset($conf['field.']);
453 }
454
455 // Check required fields
456 if ($conf['language'] === '') {
457 throw new \InvalidArgumentException('Argument \'language\' must be supplied.', 1522959186);
458 }
459 if ($conf['field'] === '') {
460 throw new \InvalidArgumentException('Argument \'field\' must be supplied.', 1522959187);
461 }
462
463 // Get and check current site
464 $site = $this->getCurrentSite();
465
466 // Throws InvalidArgumentException in case language is not found which is fine
467 $language = $site->getLanguageById((int)$conf['language']);
468 if ($language->enabled()) {
469 $language = $language->toArray();
470 } else {
471 $language = null;
472 }
473
474 // Check field for return exists
475 if ($language !== null && !isset($language[$conf['field']])) {
476 throw new \InvalidArgumentException('Invalid value \'' . $conf['field'] . '\' for argument \'field\' supplied.', 1524063160);
477 }
478
479 return $this->jsonEncode($language[$conf['field']]);
480 }
481 }