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