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