[CLEANUP] Use Permission constants consistently
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / Wizard / NewContentElementWizardController.php
1 <?php
2 declare(strict_types=1);
3
4 namespace TYPO3\CMS\Backend\Controller\Wizard;
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 Psr\Http\Message\ResponseInterface;
20 use Psr\Http\Message\ServerRequestInterface;
21 use TYPO3\CMS\Backend\Template\ModuleTemplate;
22 use TYPO3\CMS\Backend\Tree\View\ContentCreationPagePositionMap;
23 use TYPO3\CMS\Backend\Utility\BackendUtility;
24 use TYPO3\CMS\Backend\View\BackendLayoutView;
25 use TYPO3\CMS\Backend\Wizard\NewContentElementWizardHookInterface;
26 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27 use TYPO3\CMS\Core\Imaging\Icon;
28 use TYPO3\CMS\Core\Localization\LanguageService;
29 use TYPO3\CMS\Core\Service\DependencyOrderingService;
30 use TYPO3\CMS\Core\Type\Bitmask\Permission;
31 use TYPO3\CMS\Core\Utility\ArrayUtility;
32 use TYPO3\CMS\Core\Utility\GeneralUtility;
33 use TYPO3\CMS\Fluid\View\StandaloneView;
34
35 /**
36 * Script Class for the New Content element wizard
37 */
38 class NewContentElementWizardController
39 {
40 /**
41 * ModuleTemplate object
42 *
43 * @var ModuleTemplate
44 */
45 protected $moduleTemplate;
46
47 /**
48 * Page id
49 *
50 * @var int
51 */
52 protected $id;
53
54 /**
55 * Sys language
56 *
57 * @var int
58 */
59 protected $sysLanguage = 0;
60
61 /**
62 * Return URL.
63 *
64 * @var string
65 */
66 protected $returnUrl = '';
67
68 /**
69 * If set, the content is destined for a specific column.
70 *
71 * @var int|null
72 */
73 protected $colPos;
74
75 /**
76 * @var int
77 */
78 protected $uidPid;
79
80 /**
81 * Module TSconfig.
82 *
83 * @var array
84 */
85 protected $modTsConfig = [];
86
87 /**
88 * Used to accumulate the content of the module.
89 *
90 * @var string
91 */
92 protected $content;
93
94 /**
95 * Access boolean.
96 *
97 * @var bool
98 */
99 protected $access;
100
101 /**
102 * config of the wizard
103 *
104 * @var array
105 */
106 protected $config;
107
108 /**
109 * @var array
110 */
111 protected $pageInfo;
112
113 /**
114 * @var string
115 */
116 protected $onClickEvent;
117
118 /**
119 * @var array
120 */
121 protected $moduleConfiguration;
122
123 /**
124 * @var StandaloneView
125 */
126 protected $view;
127
128 /**
129 * @var StandaloneView
130 */
131 protected $menuItemView;
132
133 /**
134 * PSR Request Object
135 *
136 * @var ServerRequestInterface
137 */
138 protected $request;
139
140 /**
141 * Constructor
142 */
143 public function __construct()
144 {
145 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
146 $GLOBALS['SOBE'] = $this;
147 $this->view = $this->getFluidTemplateObject();
148 $this->menuItemView = $this->getFluidTemplateObject('MenuItem.html');
149 $this->init();
150 }
151
152 /**
153 * returns a new standalone view, shorthand function
154 *
155 * @param string $filename
156 * @return StandaloneView
157 */
158 protected function getFluidTemplateObject(string $filename = 'Main.html'): StandaloneView
159 {
160 /** @var StandaloneView $view */
161 $view = GeneralUtility::makeInstance(StandaloneView::class);
162 $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/NewContentElement/' . $filename));
163 $view->getRequest()->setControllerExtensionName('Backend');
164 return $view;
165 }
166
167 /**
168 * Constructor, initializing internal variables.
169 */
170 protected function init()
171 {
172 $lang = $this->getLanguageService();
173 $lang->includeLLFile('EXT:lang/Resources/Private/Language/locallang_misc.xlf');
174 $originalLocalLanguage = $GLOBALS['LOCAL_LANG'];
175 $lang->includeLLFile('EXT:backend/Resources/Private/Language/locallang_db_new_content_el.xlf');
176 ArrayUtility::mergeRecursiveWithOverrule($originalLocalLanguage, $GLOBALS['LOCAL_LANG']);
177 $GLOBALS['LOCAL_LANG'] = $originalLocalLanguage;
178
179 // Setting internal vars:
180 $this->id = (int)GeneralUtility::_GP('id');
181 $this->sysLanguage = (int)GeneralUtility::_GP('sys_language_uid');
182 $this->returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
183 // original variable needs to be kept as is for position map
184 $GLOBALS['SOBE']->R_URI = $this->returnUrl;
185 $this->colPos = GeneralUtility::_GP('colPos') === null ? null : (int)GeneralUtility::_GP('colPos');
186 $this->uidPid = (int)GeneralUtility::_GP('uid_pid');
187 $this->moduleConfiguration['name'] = 'xMOD_db_new_content_el';
188 $this->modTsConfig = BackendUtility::getModTSconfig($this->id, 'mod.wizards.newContentElement');
189 $configuration = BackendUtility::getPagesTSconfig($this->id);
190 $this->configuration = $configuration['mod.']['wizards.']['newContentElement.'];
191 // Getting the current page and receiving access information (used in main())
192 $permissionsClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
193 $this->pageInfo = BackendUtility::readPageAccess($this->id, $permissionsClause);
194 $this->access = is_array($this->pageInfo);
195 }
196
197 /**
198 * Injects the request object for the current request or subrequest
199 * As this controller goes only through the main() method, it is rather simple for now
200 *
201 * @param ServerRequestInterface $request the current request
202 * @param ResponseInterface $response
203 * @return ResponseInterface the response with the content
204 */
205 public function mainAction(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
206 {
207 $this->main();
208 $response->getBody()->write($this->content);
209 return $response;
210 }
211
212 /**
213 * Creating the module output.
214 *
215 * @throws \UnexpectedValueException
216 */
217 protected function main()
218 {
219 $hasAccess = true;
220 if ($this->id && $this->access) {
221
222 // Init position map object:
223 $positionMap = GeneralUtility::makeInstance(ContentCreationPagePositionMap::class);
224 $positionMap->cur_sys_language = $this->sysLanguage;
225 // If a column is pre-set:
226 if (isset($this->colPos)) {
227 if ($this->uidPid < 0) {
228 $row = [];
229 $row['uid'] = abs($this->uidPid);
230 } else {
231 $row = '';
232 }
233 $this->onClickEvent = $positionMap->onClickInsertRecord(
234 $row,
235 $this->colPos,
236 '',
237 $this->uidPid,
238 $this->sysLanguage
239 );
240 } else {
241 $this->onClickEvent = '';
242 }
243 // ***************************
244 // Creating content
245 // ***************************
246 // Wizard
247 $wizardItems = $this->getWizardItems();
248 // Wrapper for wizards
249 // Hook for manipulating wizardItems, wrapper, onClickEvent etc.
250 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms']['db_new_content_el']['wizardItemsHook'] ?? [] as $className) {
251 $hookObject = GeneralUtility::makeInstance($className);
252 if (!$hookObject instanceof NewContentElementWizardHookInterface) {
253 throw new \UnexpectedValueException(
254 $className . ' must implement interface ' . NewContentElementWizardHookInterface::class,
255 1496496724
256 );
257 }
258 $hookObject->manipulateWizardItems($wizardItems, $this);
259 }
260
261 // Traverse items for the wizard.
262 // An item is either a header or an item rendered with a radio button and title/description and icon:
263 $counter = ($key = 0);
264 $menuItems = [];
265
266 $this->view->assign('onClickEvent', $this->onClickEvent);
267
268 foreach ($wizardItems as $wizardKey => $wizardInformation) {
269 $wizardOnClick = '';
270 if ($wizardInformation['header']) {
271 $menuItems[] = [
272 'label' => $wizardInformation['header'],
273 'content' => ''
274 ];
275 $key = count($menuItems) - 1;
276 } else {
277 if (!$this->onClickEvent) {
278 // Radio button:
279 $wizardOnClick = 'document.editForm.defValues.value=unescape(' . GeneralUtility::quoteJSvalue(rawurlencode($wizardInformation['params'])) . '); window.location.hash=\'#sel2\';';
280 // Onclick action for icon/title:
281 $actionOnClick = 'document.getElementsByName(\'tempB\')[' . $counter . '].checked=1;' . $wizardOnClick . 'return false;';
282 } else {
283 $actionOnClick = 'document.editForm.defValues.value=unescape("' . rawurlencode($wizardInformation['params']) . '");goToalt_doc();' . (!$this->onClickEvent ? 'window.location.hash=\'#sel2\';' : '');
284 }
285
286 $icon = $this->moduleTemplate->getIconFactory()->getIcon($wizardInformation['iconIdentifier'])->render();
287
288 $this->menuItemView->assignMultiple(
289 [
290 'onClickEvent' => $this->onClickEvent,
291 'aOnClick' => $actionOnClick,
292 'wizardInformation' => $wizardInformation,
293 'icon' => $icon,
294 'wizardOnClick' => $wizardOnClick,
295 'wizardKey' => $wizardKey
296 ]
297 );
298 $menuItems[$key]['content'] .= $this->menuItemView->render();
299 $counter++;
300 }
301 }
302
303 $this->view->assign('renderedTabs', $this->moduleTemplate->getDynamicTabMenu(
304 $menuItems,
305 'new-content-element-wizard'
306 ));
307
308 // If the user must also select a column:
309 if (!$this->onClickEvent) {
310
311 // Load SHARED page-TSconfig settings and retrieve column list from there, if applicable:
312 $colPosArray = GeneralUtility::callUserFunction(
313 BackendLayoutView::class . '->getColPosListItemsParsed',
314 $this->id,
315 $this
316 );
317 $colPosIds = array_column($colPosArray, 1);
318 // Removing duplicates, if any
319 $colPosList = implode(',', array_unique(array_map('intval', $colPosIds)));
320 // Finally, add the content of the column selector to the content:
321 $this->view->assign(
322 'posMap',
323 $positionMap->printContentElementColumns($this->id, 0, $colPosList, 1, $this->returnUrl)
324 );
325 }
326 } else {
327 // In case of no access:
328 $hasAccess = false;
329 }
330 $this->view->assign('hasAccess', $hasAccess);
331
332 $this->content = $this->view->render();
333 // Setting up the buttons and markers for docheader
334 $this->getButtons();
335 }
336
337 /**
338 * Returns the array of elements in the wizard display.
339 * For the plugin section there is support for adding elements there from a global variable.
340 *
341 * @return array
342 */
343 protected function getWizardItems(): array
344 {
345 $wizardItems = [];
346 if (isset($this->configuration['wizardItems.'])) {
347 $wizards = $this->configuration['wizardItems.'];
348 if (empty($wizards['elements.'])) {
349 $wizards['elements.'] = [];
350 }
351 $appendWizards = $this->appendWizards($wizards['elements.']);
352 if (is_array($wizards)) {
353 foreach ($wizards as $groupKey => $wizardGroup) {
354 if (is_array($wizards[$groupKey])) {
355 $this->prepareDependencyOrdering($wizards[$groupKey], 'before');
356 $this->prepareDependencyOrdering($wizards[$groupKey], 'after');
357 }
358 }
359 $wizards = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($wizards);
360
361 foreach ($wizards as $groupKey => $wizardGroup) {
362 if (is_array($wizardGroup)) {
363 $groupKey = rtrim($groupKey, '.');
364 $showItems = GeneralUtility::trimExplode(',', $wizardGroup['show'], true);
365 $showAll = in_array('*', $showItems, true);
366 $groupItems = [];
367 if (is_array($appendWizards[$groupKey . '.']['elements.'])) {
368 $wizardElements = array_merge(
369 (array)$wizardGroup['elements.'],
370 $appendWizards[$groupKey . '.']['elements.']
371 );
372 } else {
373 $wizardElements = $wizardGroup['elements.'];
374 }
375 if (is_array($wizardElements)) {
376 foreach ($wizardElements as $itemKey => $itemConfiguration) {
377 $itemKey = rtrim($itemKey, '.');
378 if (($showAll || in_array($itemKey, $showItems) && is_array($itemConfiguration))) {
379 $item = $this->getItem($itemConfiguration);
380 if ($item) {
381 $groupItems[$groupKey . '_' . $itemKey] = $item;
382 }
383 }
384 }
385 }
386 if (!empty($groupItems)) {
387 $wizardItems[$groupKey] = $this->getGroupHeader($wizardGroup);
388 $wizardItems = array_merge($wizardItems, $groupItems);
389 }
390 }
391 }
392 }
393 }
394 // Remove elements where preset values are not allowed:
395 $this->removeInvalidElements($wizardItems);
396 return $wizardItems;
397 }
398
399 /**
400 * @param array $wizardElements
401 * @return array $returnElements
402 */
403 protected function appendWizards(array $wizardElements): array
404 {
405 if (is_array($GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses'])) {
406 foreach ($GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses'] as $class => $path) {
407 if (!class_exists($class) && file_exists($path)) {
408 require_once $path;
409 }
410 $moduleObject = GeneralUtility::makeInstance($class);
411 if (method_exists($moduleObject, 'proc')) {
412 $wizardElements = $moduleObject->proc($wizardElements);
413 }
414 }
415 }
416 $returnElements = [];
417 foreach ($wizardElements as $key => $wizardItem) {
418 preg_match('/^[a-zA-Z0-9]+_/', $key, $group);
419 $wizardGroup = $group[0] ? substr($group[0], 0, -1) . '.' : $key;
420 $returnElements[$wizardGroup]['elements.'][substr($key, strlen($wizardGroup)) . '.'] = $wizardItem;
421 }
422 return $returnElements;
423 }
424
425 /**
426 * Prepare a wizard tab configuration for sorting.
427 *
428 * @param array $wizardGroup TypoScript wizard tab configuration
429 * @param string $key Which array key should be prepared
430 */
431 protected function prepareDependencyOrdering(array &$wizardGroup, string $key)
432 {
433 if (isset($wizardGroup[$key])) {
434 $wizardGroup[$key] = GeneralUtility::trimExplode(',', $wizardGroup[$key]);
435 $wizardGroup[$key] = array_map(function ($s) {
436 return $s . '.';
437 }, $wizardGroup[$key]);
438 }
439 }
440
441 /**
442 * @param array $itemConfiguration
443 * @return array $itemConfiguration
444 */
445 protected function getItem(array $itemConfiguration): array
446 {
447 $itemConfiguration['title'] = $this->getLanguageService()->sL($itemConfiguration['title']);
448 $itemConfiguration['description'] = $this->getLanguageService()->sL($itemConfiguration['description']);
449 $itemConfiguration['tt_content_defValues'] = $itemConfiguration['tt_content_defValues.'];
450 unset($itemConfiguration['tt_content_defValues.']);
451 return $itemConfiguration;
452 }
453
454 /**
455 * @param array $wizardGroup
456 * @return array
457 */
458 protected function getGroupHeader(array $wizardGroup): array
459 {
460 return [
461 'header' => $this->getLanguageService()->sL($wizardGroup['header'])
462 ];
463 }
464
465 /**
466 * Checks the array for elements which might contain unallowed default values and will unset them!
467 * Looks for the "tt_content_defValues" key in each element and if found it will traverse that array as fieldname /
468 * value pairs and check.
469 * The values will be added to the "params" key of the array (which should probably be unset or empty by default).
470 *
471 * @param array $wizardItems Wizard items, passed by reference
472 */
473 protected function removeInvalidElements(&$wizardItems)
474 {
475 // Get TCEFORM from TSconfig of current page
476 $row = ['pid' => $this->id];
477 $tceFormTsConfig = BackendUtility::getTCEFORM_TSconfig('tt_content', $row);
478 $headersUsed = [];
479 // Traverse wizard items:
480 foreach ($wizardItems as $key => $configuration) {
481 // Exploding parameter string, if any (old style)
482 if ($wizardItems[$key]['params']) {
483 // Explode GET vars recursively
484 $temporaryGetVariables = GeneralUtility::explodeUrl2Array($wizardItems[$key]['params'], true);
485 // If tt_content values are set, merge them into the tt_content_defValues array,
486 // unset them from $temporaryGetVariables and re-implode $temporaryGetVariables into the param string
487 // (in case remaining parameters are around).
488 if (is_array($temporaryGetVariables['defVals']['tt_content'])) {
489 $wizardItems[$key]['tt_content_defValues'] = array_merge(
490 is_array($wizardItems[$key]['tt_content_defValues']) ? $wizardItems[$key]['tt_content_defValues'] : [],
491 $temporaryGetVariables['defVals']['tt_content']
492 );
493 unset($temporaryGetVariables['defVals']['tt_content']);
494 $wizardItems[$key]['params'] = GeneralUtility::implodeArrayForUrl('', $temporaryGetVariables);
495 }
496 }
497 // If tt_content_defValues are defined...:
498 if (is_array($wizardItems[$key]['tt_content_defValues'])) {
499 $backendUser = $this->getBackendUser();
500 // Traverse field values:
501 foreach ($wizardItems[$key]['tt_content_defValues'] as $fieldName => $fieldValue) {
502 if (is_array($GLOBALS['TCA']['tt_content']['columns'][$fieldName])) {
503 // Get information about if the field value is OK:
504 $configuration = &$GLOBALS['TCA']['tt_content']['columns'][$fieldName]['config'];
505 $authenticationModeDeny = $configuration['type'] === 'select' && $configuration['authMode']
506 && !$backendUser->checkAuthMode(
507 'tt_content',
508 $fieldName,
509 $fieldValue,
510 $configuration['authMode']
511 );
512 // explode TSconfig keys only as needed
513 if (!isset($removeItems[$fieldName])) {
514 $removeItems[$fieldName] = GeneralUtility::trimExplode(
515 ',',
516 $tceFormTsConfig[$fieldName]['removeItems'],
517 true
518 );
519 }
520 if (!isset($keepItems[$fieldName])) {
521 $keepItems[$fieldName] = GeneralUtility::trimExplode(
522 ',',
523 $tceFormTsConfig[$fieldName]['keepItems'],
524 true
525 );
526 }
527 $isNotInKeepItems = !empty($keepItems[$fieldName]) && !in_array(
528 $fieldValue,
529 $keepItems[$fieldName]
530 );
531 if ($authenticationModeDeny || $fieldName === 'CType' && (in_array(
532 $fieldValue,
533 $removeItems[$fieldName]
534 ) || $isNotInKeepItems)
535 ) {
536 // Remove element all together:
537 unset($wizardItems[$key]);
538 break;
539 }
540 // Add the parameter:
541 $wizardItems[$key]['params'] .= '&defVals[tt_content][' . $fieldName . ']=' . rawurlencode($this->getLanguageService()->sL($fieldValue));
542 $headerKey = explode('_', $key);
543 $headersUsed[$headerKey[0]] = $headerKey[0];
544 }
545 }
546 }
547 }
548 // remove headers without elements
549 foreach ($wizardItems as $key => $configuration) {
550 $headerKey = explode('_', $key);
551 if ($headerKey[0] && !$headerKey[1] && !in_array($headerKey[0], $headersUsed)) {
552 unset($wizardItems[$key]);
553 }
554 }
555 }
556
557 /**
558 * Create the panel of buttons for submitting the form or otherwise perform operations.
559 */
560 protected function getButtons()
561 {
562 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
563 if ($this->returnUrl) {
564 $backButton = $buttonBar->makeLinkButton()
565 ->setHref($this->returnUrl)
566 ->setTitle($this->getLanguageService()->getLL('goBack'))
567 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
568 'actions-view-go-back',
569 Icon::SIZE_SMALL
570 ));
571 $buttonBar->addButton($backButton);
572 }
573 $contextSensitiveHelpButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('new_ce');
574 $buttonBar->addButton($contextSensitiveHelpButton);
575 }
576
577 /**
578 * Returns LanguageService
579 *
580 * @return LanguageService
581 */
582 protected function getLanguageService(): LanguageService
583 {
584 return $GLOBALS['LANG'];
585 }
586
587 /**
588 * Returns the current BE user.
589 *
590 * @return BackendUserAuthentication
591 */
592 protected function getBackendUser(): BackendUserAuthentication
593 {
594 return $GLOBALS['BE_USER'];
595 }
596 }