[!!!][FEATURE] Streamline Fluid Styled Content and CSS Styled Content
[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 */
59 class MenuProcessor implements DataProcessorInterface
60 {
61 const LINK_PLACEHOLDER = '###LINKPLACEHOLDER###';
62 const TARGET_PLACEHOLDER = '###TARGETPLACEHOLDER###';
63
64 /**
65 * The content object renderer
66 *
67 * @var ContentObjectRenderer
68 */
69 public $cObj;
70
71 /**
72 * The processor configuration
73 *
74 * @var array
75 */
76 protected $processorConfiguration;
77
78 /**
79 * Allowed configuration keys for menu generation, other keys
80 * will throw an exception to prevent configuration errors.
81 *
82 * @var array
83 */
84 public $allowedConfigurationKeys = [
85 'cache_period',
86 'entryLevel',
87 'entryLevel.',
88 'special',
89 'special.',
90 'minItems',
91 'minItems.',
92 'maxItems',
93 'maxItems.',
94 'begin',
95 'begin.',
96 'alternativeSortingField',
97 'alternativeSortingField.',
98 'excludeUidList',
99 'excludeUidList.',
100 'excludeDoktypes',
101 'includeNotInMenu',
102 'alwaysActivePIDlist',
103 'alwaysActivePIDlist.',
104 'protectLvar',
105 'addQueryString',
106 'if',
107 'if.',
108 'levels',
109 'expandAll',
110 'includeSpacer',
111 'as',
112 'titleField',
113 'dataProcessing',
114 'dataProcessing.'
115 ];
116
117 /**
118 * Remove keys from configuration that should not be passed
119 * to HMENU to prevent configuration errors
120 *
121 * @var array
122 */
123 public $removeConfigurationKeysForHmenu = [
124 'levels',
125 'expandAll',
126 'includeSpacer',
127 'as',
128 'titleField',
129 'dataProcessing',
130 'dataProcessing.'
131 ];
132
133 /**
134 * @var array
135 */
136 protected $menuConfig = [
137 'wrap' => '[|]'
138 ];
139
140 /**
141 * @var array
142 */
143 protected $menuLevelConfig = [
144 'doNotLinkIt' => '1',
145 'wrapItemAndSub' => '{|}, |*| {|}, |*| {|}',
146 'stdWrap.' => [
147 'cObject' => 'COA',
148 'cObject.' => [
149 '10' => 'USER',
150 '10.' => [
151 'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\MenuProcessor->getDataAsJson',
152 'stdWrap.' => [
153 'wrap' => '"data":|'
154 ]
155 ],
156 '20' => 'TEXT',
157 '20.' => [
158 'field' => 'nav_title // title',
159 'trim' => '1',
160 'wrap' => ',"title":|',
161 'preUserFunc' => 'TYPO3\CMS\Frontend\DataProcessing\MenuProcessor->jsonEncodeUserFunc'
162 ],
163 '21' => 'TEXT',
164 '21.' => [
165 'value' => self::LINK_PLACEHOLDER,
166 'wrap' => ',"link":|',
167 ],
168 '22' => 'TEXT',
169 '22.' => [
170 'value' => self::TARGET_PLACEHOLDER,
171 'wrap' => ',"target":|',
172 ],
173 '30' => 'TEXT',
174 '30.' => [
175 'value' => '0',
176 'wrap' => ',"active":|'
177 ],
178 '40' => 'TEXT',
179 '40.' => [
180 'value' => '0',
181 'wrap' => ',"current":|'
182 ],
183 '50' => 'TEXT',
184 '50.' => [
185 'value' => '0',
186 'wrap' => ',"spacer":|'
187 ]
188 ]
189 ]
190 ];
191
192 /**
193 * @var array
194 */
195 public $menuDefaults = [
196 'levels' => 1,
197 'expandAll' => 1,
198 'includeSpacer' => 0,
199 'as' => 'menu',
200 'titleField' => 'nav_title // title'
201 ];
202
203 /**
204 * @var int
205 */
206 protected $menuLevels;
207
208 /**
209 * @var int
210 */
211 protected $menuExpandAll;
212
213 /**
214 * @var int
215 */
216 protected $menuIncludeSpacer;
217
218 /**
219 * @var string
220 */
221 protected $menuTitleField;
222
223 /**
224 * @var string
225 */
226 protected $menuAlternativeSortingField;
227
228 /**
229 * @var string
230 */
231 protected $menuTargetVariableName;
232
233 /**
234 * @var ContentDataProcessor
235 */
236 protected $contentDataProcessor;
237
238 /**
239 * Constructor
240 */
241 public function __construct()
242 {
243 $this->contentDataProcessor = GeneralUtility::makeInstance(ContentDataProcessor::class);
244 }
245
246 /**
247 * Get configuration value from processorConfiguration
248 *
249 * @param string $key
250 * @return string
251 */
252 protected function getConfigurationValue($key)
253 {
254 return $this->cObj->stdWrapValue($key, $this->processorConfiguration, $this->menuDefaults[$key]);
255 }
256
257 /**
258 * Validate configuration
259 *
260 * @throws \InvalidArgumentException
261 */
262 public function validateConfiguration()
263 {
264 $invalidArguments = [];
265 foreach ($this->processorConfiguration as $key => $value) {
266 if (!in_array($key, $this->allowedConfigurationKeys)) {
267 $invalidArguments[str_replace('.', '', $key)] = $key;
268 }
269 }
270 if (!empty($invalidArguments)) {
271 throw new \InvalidArgumentException('MenuProcessor Configuration contains invalid Arguments: ' . implode(', ', $invalidArguments), 1478806566);
272 }
273 }
274
275 /**
276 * Prepare Configuration
277 */
278 public function prepareConfiguration()
279 {
280 $this->menuConfig += $this->processorConfiguration;
281 // Filter configuration
282 foreach ($this->menuConfig as $key => $value) {
283 if (in_array($key, $this->removeConfigurationKeysForHmenu)) {
284 unset($this->menuConfig[$key]);
285 }
286 }
287 // Process special value
288 if (isset($this->menuConfig['special.']['value.'])) {
289 $this->menuConfig['special.']['value'] = $this->cObj->stdWrap($this->menuConfig['special.']['value'], $this->menuConfig['special.']['value.']);
290 unset($this->menuConfig['special.']['value.']);
291 }
292 }
293
294 /**
295 * @return void
296 */
297 public function prepareLevelConfiguration()
298 {
299 $this->menuLevelConfig['stdWrap.']['cObject.'] = array_replace_recursive(
300 $this->menuLevelConfig['stdWrap.']['cObject.'],
301 [
302 '20.' => [
303 'field' => $this->menuTitleField,
304 ]
305 ]
306 );
307 }
308
309 /**
310 * @return void
311 */
312 public function prepareLevelLanguageConfiguration()
313 {
314 if ($this->menuConfig['special'] === 'language') {
315 $this->menuLevelConfig['stdWrap.']['cObject.'] = array_replace_recursive(
316 $this->menuLevelConfig['stdWrap.']['cObject.'],
317 [
318 '60' => 'TEXT',
319 '60.' => [
320 'value' => '1',
321 'wrap' => ',"available":|'
322 ],
323 '70' => 'TEXT',
324 '70.' => [
325 'value' => $this->menuConfig['special.']['value'],
326 'listNum.' => [
327 'stdWrap.' => [
328 'data' => 'register:count_HMENU_MENUOBJ',
329 'wrap' => '|-1'
330 ],
331 'splitChar' => ','
332 ],
333 'wrap' => ',"languageUid":"|"'
334 ]
335 ]
336 );
337 }
338 }
339
340 /**
341 * @return void
342 */
343 public function buildConfiguration()
344 {
345 for ($i = 1; $i <= $this->menuLevels; $i++) {
346 $this->menuConfig[$i] = 'TMENU';
347 $this->menuConfig[$i . '.']['IProcFunc'] = 'TYPO3\CMS\Frontend\DataProcessing\MenuProcessor->replacePlaceholderInRenderedMenuItem';
348 if ($i > 1) {
349 $this->menuConfig[$i . '.']['stdWrap.']['wrap'] = ',"children": [|]';
350 }
351 $this->menuConfig[$i . '.']['expAll'] = $this->menuExpandAll;
352 $this->menuConfig[$i . '.']['alternativeSortingField'] = $this->menuAlternativeSortingField;
353 $this->menuConfig[$i . '.']['NO'] = '1';
354 $this->menuConfig[$i . '.']['NO.'] = $this->menuLevelConfig;
355 if ($this->menuIncludeSpacer) {
356 $this->menuConfig[$i . '.']['SPC'] = '1';
357 $this->menuConfig[$i . '.']['SPC.'] = $this->menuConfig[$i . '.']['NO.'];
358 $this->menuConfig[$i . '.']['SPC.']['stdWrap.']['cObject.']['50.']['value'] = '1';
359 }
360 $this->menuConfig[$i . '.']['IFSUB'] = '1';
361 $this->menuConfig[$i . '.']['IFSUB.'] = $this->menuConfig[$i . '.']['NO.'];
362 $this->menuConfig[$i . '.']['ACT'] = '1';
363 $this->menuConfig[$i . '.']['ACT.'] = $this->menuConfig[$i . '.']['NO.'];
364 $this->menuConfig[$i . '.']['ACT.']['stdWrap.']['cObject.']['30.']['value'] = '1';
365 $this->menuConfig[$i . '.']['ACTIFSUB'] = '1';
366 $this->menuConfig[$i . '.']['ACTIFSUB.'] = $this->menuConfig[$i . '.']['ACT.'];
367 $this->menuConfig[$i . '.']['CUR'] = '1';
368 $this->menuConfig[$i . '.']['CUR.'] = $this->menuConfig[$i . '.']['ACT.'];
369 $this->menuConfig[$i . '.']['CUR.']['stdWrap.']['cObject.']['40.']['value'] = '1';
370 $this->menuConfig[$i . '.']['CURIFSUB'] = '1';
371 $this->menuConfig[$i . '.']['CURIFSUB.'] = $this->menuConfig[$i . '.']['CUR.'];
372 if ($this->menuConfig['special'] === 'language') {
373 $this->menuConfig[$i . '.']['USERDEF1'] = $this->menuConfig[$i . '.']['NO'];
374 $this->menuConfig[$i . '.']['USERDEF1.'] = $this->menuConfig[$i . '.']['NO.'];
375 $this->menuConfig[$i . '.']['USERDEF1.']['stdWrap.']['cObject.']['60.']['value'] = '0';
376 $this->menuConfig[$i . '.']['USERDEF2'] = $this->menuConfig[$i . '.']['ACT'];
377 $this->menuConfig[$i . '.']['USERDEF2.'] = $this->menuConfig[$i . '.']['ACT.'];
378 $this->menuConfig[$i . '.']['USERDEF2.']['stdWrap.']['cObject.']['60.']['value'] = '0';
379 }
380 }
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)
391 {
392 $this->cObj = $cObj;
393 $this->processorConfiguration = $processorConfiguration;
394
395 // Get Configuration
396 $this->menuLevels = (int)$this->getConfigurationValue('levels') ?: 1;
397 $this->menuExpandAll = (int)$this->getConfigurationValue('expandAll');
398 $this->menuIncludeSpacer = (int)$this->getConfigurationValue('includeSpacer');
399 $this->menuTargetVariableName = $this->getConfigurationValue('as');
400 $this->menuTitleField = $this->getConfigurationValue('titleField');
401 $this->menuAlternativeSortingField = $this->getConfigurationValue('alternativeSortingField');
402
403 // Validate Configuration
404 $this->validateConfiguration();
405
406 // Build Configuration
407 $this->prepareConfiguration();
408 $this->prepareLevelConfiguration();
409 $this->prepareLevelLanguageConfiguration();
410 $this->buildConfiguration();
411
412 // Process Configuration
413 $menuContentObject = $cObj->getContentObject('HMENU');
414 $renderedMenu = $menuContentObject->render($this->menuConfig);
415 if (!$renderedMenu) {
416 return $processedData;
417 }
418
419 // Process menu
420 $menu = json_decode($renderedMenu, true);
421 $processedMenu = [];
422
423 foreach ($menu as $key => $page) {
424 $processedMenu[$key] = $this->processAdditionalDataProcessors($page, $processorConfiguration);
425 }
426
427 // Return processed data
428 $processedData[$this->menuTargetVariableName] = $processedMenu;
429 return $processedData;
430 }
431
432 /**
433 * Process additional data processors
434 *
435 * @param array $page
436 * @param array $processorConfiguration
437 */
438 protected function processAdditionalDataProcessors($page, $processorConfiguration)
439 {
440 if (is_array($page['children'])) {
441 foreach ($page['children'] as $key => $item) {
442 $page['children'][$key] = $this->processAdditionalDataProcessors($item, $processorConfiguration);
443 }
444 }
445 /** @var ContentObjectRenderer $recordContentObjectRenderer */
446 $recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
447 $recordContentObjectRenderer->start($page['data'], 'pages');
448 $processedPage = $this->contentDataProcessor->process($recordContentObjectRenderer, $processorConfiguration, $page);
449 return $processedPage;
450 }
451
452 /**
453 * Gets the data of the current record in JSON format
454 *
455 * @return string JSON encoded data
456 */
457 public function getDataAsJson()
458 {
459 return $this->jsonEncode($this->cObj->data);
460 }
461
462 /**
463 * This UserFunc encodes the content as Json
464 *
465 * @param string $content
466 * @param array $conf
467 * @return string JSON encoded content
468 */
469 public function jsonEncodeUserFunc($content, $conf)
470 {
471 $content = $this->jsonEncode($content);
472 return $content;
473 }
474
475 /**
476 * JSON Encode
477 *
478 * @param mixed $value
479 * @return string
480 */
481 public function jsonEncode($value)
482 {
483 return json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE);
484 }
485
486 /**
487 * This UserFunc gets the link and the target
488 *
489 * @param array $menuItem
490 * @param array $conf
491 * @return void
492 */
493 public function replacePlaceholderInRenderedMenuItem($menuItem, $conf)
494 {
495 $link = $this->jsonEncode($menuItem['linkHREF']['HREF']);
496 $target = $this->jsonEncode($menuItem['linkHREF']['TARGET']);
497
498 $menuItem['parts']['title'] = str_replace(self::LINK_PLACEHOLDER, $link, $menuItem['parts']['title']);
499 $menuItem['parts']['title'] = str_replace(self::TARGET_PLACEHOLDER, $target, $menuItem['parts']['title']);
500
501 return $menuItem;
502 }
503 }