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