693b64ad359b16f12f8bbcaf6b6b4229407ba973
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / DataProcessing / MenuProcessor.php
1 <?php
2 namespace TYPO3\CMS\Frontend\DataProcessing;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;
19 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
20 use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
21
22 /**
23 * This menu processor utilizes HMENU to generate a json encoded menu
24 * string that will be decoded again and assigned to FLUIDTEMPLATE as
25 * variable. Additional DataProcessing is supported and will be applied
26 * to each record.
27 *
28 * Options:
29 * as - The variable to be used within the result
30 * levels - Number of levels of the menu
31 * expandAll = If false, submenus will only render if the parent page is active
32 * includeSpacer = If true, pagetype spacer will be included in the menu
33 * titleField = Field that should be used for the title
34 *
35 * See HMENU docs for more options.
36 * https://docs.typo3.org/typo3cms/TyposcriptReference/ContentObjects/Hmenu/Index.html
37 *
38 *
39 * Example TypoScript configuration:
40 *
41 * 10 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
42 * 10 {
43 * special = list
44 * special.value.field = pages
45 * levels = 7
46 * as = menu
47 * expandAll = 1
48 * includeSpacer = 1
49 * titleField = nav_title // title
50 * dataProcessing {
51 * 10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
52 * 10 {
53 * references.fieldName = media
54 * }
55 * }
56 * }
57 */
58 class MenuProcessor implements DataProcessorInterface
59 {
60 const LINK_PLACEHOLDER = '###LINKPLACEHOLDER###';
61 const TARGET_PLACEHOLDER = '###TARGETPLACEHOLDER###';
62
63 /**
64 * The content object renderer
65 *
66 * @var ContentObjectRenderer
67 */
68 public $cObj;
69
70 /**
71 * The processor configuration
72 *
73 * @var array
74 */
75 protected $processorConfiguration;
76
77 /**
78 * Allowed configuration keys for menu generation, other keys
79 * will throw an exception to prevent configuration errors.
80 *
81 * @var array
82 */
83 public $allowedConfigurationKeys = [
84 'cache_period',
85 'entryLevel',
86 'entryLevel.',
87 'special',
88 'special.',
89 'minItems',
90 'minItems.',
91 'maxItems',
92 'maxItems.',
93 'begin',
94 'begin.',
95 'alternativeSortingField',
96 'alternativeSortingField.',
97 'excludeUidList',
98 'excludeUidList.',
99 'excludeDoktypes',
100 'includeNotInMenu',
101 'alwaysActivePIDlist',
102 'alwaysActivePIDlist.',
103 'protectLvar',
104 'addQueryString',
105 'addQueryString.',
106 'if',
107 'if.',
108 'levels',
109 'levels.',
110 'expandAll',
111 'expandAll.',
112 'includeSpacer',
113 'includeSpacer.',
114 'as',
115 'titleField',
116 'titleField.',
117 'dataProcessing',
118 'dataProcessing.'
119 ];
120
121 /**
122 * Remove keys from configuration that should not be passed
123 * to HMENU to prevent configuration errors
124 *
125 * @var array
126 */
127 public $removeConfigurationKeysForHmenu = [
128 'levels',
129 'levels.',
130 'expandAll',
131 'expandAll.',
132 'includeSpacer',
133 'includeSpacer.',
134 'as',
135 'titleField',
136 'titleField.',
137 'dataProcessing',
138 'dataProcessing.'
139 ];
140
141 /**
142 * @var array
143 */
144 protected $menuConfig = [
145 'wrap' => '[|]'
146 ];
147
148 /**
149 * @var array
150 */
151 protected $menuLevelConfig = [
152 'doNotLinkIt' => '1',
153 'wrapItemAndSub' => '{|}, |*| {|}, |*| {|}',
154 'stdWrap.' => [
155 'cObject' => 'COA',
156 'cObject.' => [
157 '10' => 'USER',
158 '10.' => [
159 'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\MenuProcessor->getDataAsJson',
160 'stdWrap.' => [
161 'wrap' => '"data":|'
162 ]
163 ],
164 '20' => 'TEXT',
165 '20.' => [
166 'field' => 'nav_title // title',
167 'trim' => '1',
168 'wrap' => ',"title":|',
169 'preUserFunc' => 'TYPO3\CMS\Frontend\DataProcessing\MenuProcessor->jsonEncodeUserFunc'
170 ],
171 '21' => 'TEXT',
172 '21.' => [
173 'value' => self::LINK_PLACEHOLDER,
174 'wrap' => ',"link":|',
175 ],
176 '22' => 'TEXT',
177 '22.' => [
178 'value' => self::TARGET_PLACEHOLDER,
179 'wrap' => ',"target":|',
180 ],
181 '30' => 'TEXT',
182 '30.' => [
183 'value' => '0',
184 'wrap' => ',"active":|'
185 ],
186 '40' => 'TEXT',
187 '40.' => [
188 'value' => '0',
189 'wrap' => ',"current":|'
190 ],
191 '50' => 'TEXT',
192 '50.' => [
193 'value' => '0',
194 'wrap' => ',"spacer":|'
195 ]
196 ]
197 ]
198 ];
199
200 /**
201 * @var array
202 */
203 public $menuDefaults = [
204 'levels' => 1,
205 'expandAll' => 1,
206 'includeSpacer' => 0,
207 'as' => 'menu',
208 'titleField' => 'nav_title // title'
209 ];
210
211 /**
212 * @var int
213 */
214 protected $menuLevels;
215
216 /**
217 * @var int
218 */
219 protected $menuExpandAll;
220
221 /**
222 * @var int
223 */
224 protected $menuIncludeSpacer;
225
226 /**
227 * @var string
228 */
229 protected $menuTitleField;
230
231 /**
232 * @var string
233 */
234 protected $menuAlternativeSortingField;
235
236 /**
237 * @var string
238 */
239 protected $menuTargetVariableName;
240
241 /**
242 * @var ContentDataProcessor
243 */
244 protected $contentDataProcessor;
245
246 /**
247 * Constructor
248 */
249 public function __construct()
250 {
251 $this->contentDataProcessor = GeneralUtility::makeInstance(ContentDataProcessor::class);
252 }
253
254 /**
255 * Get configuration value from processorConfiguration
256 *
257 * @param string $key
258 * @return string
259 */
260 protected function getConfigurationValue($key)
261 {
262 return $this->cObj->stdWrapValue($key, $this->processorConfiguration, $this->menuDefaults[$key]);
263 }
264
265 /**
266 * Validate configuration
267 *
268 * @throws \InvalidArgumentException
269 */
270 public function validateConfiguration()
271 {
272 $invalidArguments = [];
273 foreach ($this->processorConfiguration as $key => $value) {
274 if (!in_array($key, $this->allowedConfigurationKeys)) {
275 $invalidArguments[str_replace('.', '', $key)] = $key;
276 }
277 }
278 if (!empty($invalidArguments)) {
279 throw new \InvalidArgumentException('MenuProcessor Configuration contains invalid Arguments: ' . implode(', ', $invalidArguments), 1478806566);
280 }
281 }
282
283 /**
284 * Prepare Configuration
285 */
286 public function prepareConfiguration()
287 {
288 $this->menuConfig += $this->processorConfiguration;
289 // Filter configuration
290 foreach ($this->menuConfig as $key => $value) {
291 if (in_array($key, $this->removeConfigurationKeysForHmenu)) {
292 unset($this->menuConfig[$key]);
293 }
294 }
295 // Process special value
296 if (isset($this->menuConfig['special.']['value.'])) {
297 $this->menuConfig['special.']['value'] = $this->cObj->stdWrap($this->menuConfig['special.']['value'], $this->menuConfig['special.']['value.']);
298 unset($this->menuConfig['special.']['value.']);
299 }
300 }
301
302 /**
303 * Prepare configuration for a certain menu level in the hierarchy
304 */
305 public function prepareLevelConfiguration()
306 {
307 $this->menuLevelConfig['stdWrap.']['cObject.'] = array_replace_recursive(
308 $this->menuLevelConfig['stdWrap.']['cObject.'],
309 [
310 '20.' => [
311 'field' => $this->menuTitleField,
312 ]
313 ]
314 );
315 }
316
317 /**
318 * Prepare the configuration when rendering a language menu
319 */
320 public function prepareLevelLanguageConfiguration()
321 {
322 if ($this->menuConfig['special'] === 'language') {
323 $this->menuLevelConfig['stdWrap.']['cObject.'] = array_replace_recursive(
324 $this->menuLevelConfig['stdWrap.']['cObject.'],
325 [
326 '60' => 'TEXT',
327 '60.' => [
328 'value' => '1',
329 'wrap' => ',"available":|'
330 ],
331 '70' => 'TEXT',
332 '70.' => [
333 'value' => $this->menuConfig['special.']['value'],
334 'listNum.' => [
335 'stdWrap.' => [
336 'data' => 'register:count_HMENU_MENUOBJ',
337 'wrap' => '|-1'
338 ],
339 'splitChar' => ','
340 ],
341 'wrap' => ',"languageUid":"|"'
342 ]
343 ]
344 );
345 }
346 }
347
348 /**
349 * Build the menu configuration so it can be treated by HMENU cObject
350 */
351 public function buildConfiguration()
352 {
353 for ($i = 1; $i <= $this->menuLevels; $i++) {
354 $this->menuConfig[$i] = 'TMENU';
355 $this->menuConfig[$i . '.']['IProcFunc'] = 'TYPO3\CMS\Frontend\DataProcessing\MenuProcessor->replacePlaceholderInRenderedMenuItem';
356 if ($i > 1) {
357 $this->menuConfig[$i . '.']['stdWrap.']['wrap'] = ',"children": [|]';
358 }
359 $this->menuConfig[$i . '.']['expAll'] = $this->menuExpandAll;
360 $this->menuConfig[$i . '.']['alternativeSortingField'] = $this->menuAlternativeSortingField;
361 $this->menuConfig[$i . '.']['NO'] = '1';
362 $this->menuConfig[$i . '.']['NO.'] = $this->menuLevelConfig;
363 if ($this->menuIncludeSpacer) {
364 $this->menuConfig[$i . '.']['SPC'] = '1';
365 $this->menuConfig[$i . '.']['SPC.'] = $this->menuConfig[$i . '.']['NO.'];
366 $this->menuConfig[$i . '.']['SPC.']['stdWrap.']['cObject.']['50.']['value'] = '1';
367 }
368 $this->menuConfig[$i . '.']['IFSUB'] = '1';
369 $this->menuConfig[$i . '.']['IFSUB.'] = $this->menuConfig[$i . '.']['NO.'];
370 $this->menuConfig[$i . '.']['ACT'] = '1';
371 $this->menuConfig[$i . '.']['ACT.'] = $this->menuConfig[$i . '.']['NO.'];
372 $this->menuConfig[$i . '.']['ACT.']['stdWrap.']['cObject.']['30.']['value'] = '1';
373 $this->menuConfig[$i . '.']['ACTIFSUB'] = '1';
374 $this->menuConfig[$i . '.']['ACTIFSUB.'] = $this->menuConfig[$i . '.']['ACT.'];
375 $this->menuConfig[$i . '.']['CUR'] = '1';
376 $this->menuConfig[$i . '.']['CUR.'] = $this->menuConfig[$i . '.']['ACT.'];
377 $this->menuConfig[$i . '.']['CUR.']['stdWrap.']['cObject.']['40.']['value'] = '1';
378 $this->menuConfig[$i . '.']['CURIFSUB'] = '1';
379 $this->menuConfig[$i . '.']['CURIFSUB.'] = $this->menuConfig[$i . '.']['CUR.'];
380 if ($this->menuConfig['special'] === 'language') {
381 $this->menuConfig[$i . '.']['USERDEF1'] = $this->menuConfig[$i . '.']['NO'];
382 $this->menuConfig[$i . '.']['USERDEF1.'] = $this->menuConfig[$i . '.']['NO.'];
383 $this->menuConfig[$i . '.']['USERDEF1.']['stdWrap.']['cObject.']['60.']['value'] = '0';
384 $this->menuConfig[$i . '.']['USERDEF2'] = $this->menuConfig[$i . '.']['ACT'];
385 $this->menuConfig[$i . '.']['USERDEF2.'] = $this->menuConfig[$i . '.']['ACT.'];
386 $this->menuConfig[$i . '.']['USERDEF2.']['stdWrap.']['cObject.']['60.']['value'] = '0';
387 }
388 }
389 }
390
391 /**
392 * @param ContentObjectRenderer $cObj The data of the content element or page
393 * @param array $contentObjectConfiguration The configuration of Content Object
394 * @param array $processorConfiguration The configuration of this processor
395 * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
396 * @return array the processed data as key/value store
397 */
398 public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
399 {
400 $this->cObj = $cObj;
401 $this->processorConfiguration = $processorConfiguration;
402
403 // Get Configuration
404 $this->menuLevels = (int)$this->getConfigurationValue('levels') ?: 1;
405 $this->menuExpandAll = (int)$this->getConfigurationValue('expandAll');
406 $this->menuIncludeSpacer = (int)$this->getConfigurationValue('includeSpacer');
407 $this->menuTargetVariableName = $this->getConfigurationValue('as');
408 $this->menuTitleField = $this->getConfigurationValue('titleField');
409 $this->menuAlternativeSortingField = $this->getConfigurationValue('alternativeSortingField');
410
411 // Validate Configuration
412 $this->validateConfiguration();
413
414 // Build Configuration
415 $this->prepareConfiguration();
416 $this->prepareLevelConfiguration();
417 $this->prepareLevelLanguageConfiguration();
418 $this->buildConfiguration();
419
420 // Process Configuration
421 $menuContentObject = $cObj->getContentObject('HMENU');
422 $renderedMenu = $menuContentObject->render($this->menuConfig);
423 if (!$renderedMenu) {
424 return $processedData;
425 }
426
427 // Process menu
428 $menu = json_decode($renderedMenu, true);
429 $processedMenu = [];
430
431 foreach ($menu as $key => $page) {
432 $processedMenu[$key] = $this->processAdditionalDataProcessors($page, $processorConfiguration);
433 }
434
435 // Return processed data
436 $processedData[$this->menuTargetVariableName] = $processedMenu;
437 return $processedData;
438 }
439
440 /**
441 * Process additional data processors
442 *
443 * @param array $page
444 * @param array $processorConfiguration
445 */
446 protected function processAdditionalDataProcessors($page, $processorConfiguration)
447 {
448 if (is_array($page['children'])) {
449 foreach ($page['children'] as $key => $item) {
450 $page['children'][$key] = $this->processAdditionalDataProcessors($item, $processorConfiguration);
451 }
452 }
453 /** @var ContentObjectRenderer $recordContentObjectRenderer */
454 $recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
455 $recordContentObjectRenderer->start($page['data'], 'pages');
456 $processedPage = $this->contentDataProcessor->process($recordContentObjectRenderer, $processorConfiguration, $page);
457 return $processedPage;
458 }
459
460 /**
461 * Gets the data of the current record in JSON format
462 *
463 * @return string JSON encoded data
464 */
465 public function getDataAsJson()
466 {
467 return $this->jsonEncode($this->cObj->data);
468 }
469
470 /**
471 * This UserFunc encodes the content as Json
472 *
473 * @param string $content
474 * @param array $conf
475 * @return string JSON encoded content
476 */
477 public function jsonEncodeUserFunc($content, $conf)
478 {
479 $content = $this->jsonEncode($content);
480 return $content;
481 }
482
483 /**
484 * JSON Encode
485 *
486 * @param mixed $value
487 * @return string
488 */
489 public function jsonEncode($value)
490 {
491 return json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE);
492 }
493
494 /**
495 * This UserFunc gets the link and the target
496 *
497 * @param array $menuItem
498 * @param array $conf
499 */
500 public function replacePlaceholderInRenderedMenuItem($menuItem, $conf)
501 {
502 $link = $this->jsonEncode($menuItem['linkHREF']['HREF']);
503 $target = $this->jsonEncode($menuItem['linkHREF']['TARGET']);
504
505 $menuItem['parts']['title'] = str_replace(self::LINK_PLACEHOLDER, $link, $menuItem['parts']['title']);
506 $menuItem['parts']['title'] = str_replace(self::TARGET_PLACEHOLDER, $target, $menuItem['parts']['title']);
507
508 return $menuItem;
509 }
510 }