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