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