Revert "[TASK] Avoid slow array functions in loops"
[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 = array_merge($wizardItems, $groupItems);
398 }
399 }
400 }
401 }
402 // Remove elements where preset values are not allowed:
403 $this->removeInvalidWizardItems($wizardItems);
404 return $wizardItems;
405 }
406
407 /**
408 * @param array $wizardElements
409 * @return array
410 */
411 protected function getAppendWizards(array $wizardElements): array
412 {
413 if (!is_array($wizardElements)) {
414 $wizardElements = [];
415 }
416 if (is_array($GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses'])) {
417 foreach ($GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses'] as $class => $path) {
418 if (!class_exists($class) && file_exists($path)) {
419 require_once $path;
420 }
421 $modObj = GeneralUtility::makeInstance($class);
422 if (method_exists($modObj, 'proc')) {
423 $wizardElements = $modObj->proc($wizardElements);
424 }
425 }
426 }
427 $returnElements = [];
428 foreach ($wizardElements as $key => $wizardItem) {
429 preg_match('/^[a-zA-Z0-9]+_/', $key, $group);
430 $wizardGroup = $group[0] ? substr($group[0], 0, -1) . '.' : $key;
431 $returnElements[$wizardGroup]['elements.'][substr($key, strlen($wizardGroup)) . '.'] = $wizardItem;
432 }
433 return $returnElements;
434 }
435
436 /**
437 * @param array $itemConf
438 * @return array
439 */
440 protected function getWizardItem(array $itemConf): array
441 {
442 $itemConf['title'] = $this->getLanguageService()->sL($itemConf['title']);
443 $itemConf['description'] = $this->getLanguageService()->sL($itemConf['description']);
444 $itemConf['tt_content_defValues'] = $itemConf['tt_content_defValues.'];
445 unset($itemConf['tt_content_defValues.']);
446 return $itemConf;
447 }
448
449 /**
450 * @param array $wizardGroup
451 * @return array
452 */
453 protected function getWizardGroupHeader(array $wizardGroup): array
454 {
455 return [
456 'header' => $this->getLanguageService()->sL($wizardGroup['header'])
457 ];
458 }
459
460 /**
461 * Checks the array for elements which might contain unallowed default values and will unset them!
462 * Looks for the "tt_content_defValues" key in each element and if found it will traverse that array as fieldname /
463 * value pairs and check.
464 * The values will be added to the "params" key of the array (which should probably be unset or empty by default).
465 *
466 * @param array $wizardItems Wizard items, passed by reference
467 */
468 protected function removeInvalidWizardItems(array &$wizardItems): void
469 {
470 // Get TCEFORM from TSconfig of current page
471 $row = ['pid' => $this->id];
472 $TCEFORM_TSconfig = BackendUtility::getTCEFORM_TSconfig('tt_content', $row);
473 $headersUsed = [];
474 // Traverse wizard items:
475 foreach ($wizardItems as $key => $cfg) {
476 // Exploding parameter string, if any (old style)
477 if ($wizardItems[$key]['params']) {
478 // Explode GET vars recursively
479 $tempGetVars = [];
480 parse_str($wizardItems[$key]['params'], $tempGetVars);
481 // If tt_content values are set, merge them into the tt_content_defValues array,
482 // unset them from $tempGetVars and re-implode $tempGetVars into the param string
483 // (in case remaining parameters are around).
484 if (is_array($tempGetVars['defVals']['tt_content'])) {
485 $wizardItems[$key]['tt_content_defValues'] = array_merge(
486 is_array($wizardItems[$key]['tt_content_defValues']) ? $wizardItems[$key]['tt_content_defValues'] : [],
487 $tempGetVars['defVals']['tt_content']
488 );
489 unset($tempGetVars['defVals']['tt_content']);
490 $wizardItems[$key]['params'] = HttpUtility::buildQueryString($tempGetVars, '&');
491 }
492 }
493 // If tt_content_defValues are defined...:
494 if (is_array($wizardItems[$key]['tt_content_defValues'])) {
495 $backendUser = $this->getBackendUser();
496 // Traverse field values:
497 foreach ($wizardItems[$key]['tt_content_defValues'] as $fN => $fV) {
498 if (is_array($GLOBALS['TCA']['tt_content']['columns'][$fN])) {
499 // Get information about if the field value is OK:
500 $config = &$GLOBALS['TCA']['tt_content']['columns'][$fN]['config'];
501 $authModeDeny = $config['type'] === 'select' && $config['authMode']
502 && !$backendUser->checkAuthMode('tt_content', $fN, $fV, $config['authMode']);
503 // explode TSconfig keys only as needed
504 if (!isset($removeItems[$fN]) && isset($TCEFORM_TSconfig[$fN]['removeItems']) && $TCEFORM_TSconfig[$fN]['removeItems'] !== '') {
505 $removeItems[$fN] = array_flip(GeneralUtility::trimExplode(
506 ',',
507 $TCEFORM_TSconfig[$fN]['removeItems'],
508 true
509 ));
510 }
511 if (!isset($keepItems[$fN]) && isset($TCEFORM_TSconfig[$fN]['keepItems']) && $TCEFORM_TSconfig[$fN]['keepItems'] !== '') {
512 $keepItems[$fN] = array_flip(GeneralUtility::trimExplode(
513 ',',
514 $TCEFORM_TSconfig[$fN]['keepItems'],
515 true
516 ));
517 }
518 $isNotInKeepItems = !empty($keepItems[$fN]) && !isset($keepItems[$fN][$fV]);
519 if ($authModeDeny || ($fN === 'CType' && (isset($removeItems[$fN][$fV]) || $isNotInKeepItems))) {
520 // Remove element all together:
521 unset($wizardItems[$key]);
522 break;
523 }
524 // Add the parameter:
525 $wizardItems[$key]['params'] .= '&defVals[tt_content][' . $fN . ']=' . rawurlencode($this->getLanguageService()->sL($fV));
526 $tmp = explode('_', $key);
527 $headersUsed[$tmp[0]] = $tmp[0];
528 }
529 }
530 }
531 }
532 // remove headers without elements
533 foreach ($wizardItems as $key => $cfg) {
534 $tmp = explode('_', $key);
535 if ($tmp[0] && !$tmp[1] && !in_array($tmp[0], $headersUsed)) {
536 unset($wizardItems[$key]);
537 }
538 }
539 }
540
541 /**
542 * Prepare a wizard tab configuration for sorting.
543 *
544 * @param array $wizardGroup TypoScript wizard tab configuration
545 * @param string $key Which array key should be prepared
546 */
547 protected function prepareDependencyOrdering(&$wizardGroup, $key)
548 {
549 if (isset($wizardGroup[$key])) {
550 $wizardGroup[$key] = GeneralUtility::trimExplode(',', $wizardGroup[$key]);
551 $wizardGroup[$key] = array_map(function ($s) {
552 return $s . '.';
553 }, $wizardGroup[$key]);
554 }
555 }
556
557 /**
558 * @return LanguageService
559 */
560 protected function getLanguageService(): LanguageService
561 {
562 return $GLOBALS['LANG'];
563 }
564
565 /**
566 * @return BackendUserAuthentication
567 */
568 protected function getBackendUser(): BackendUserAuthentication
569 {
570 return $GLOBALS['BE_USER'];
571 }
572
573 /**
574 * @param string $filename
575 * @return StandaloneView
576 */
577 protected function getFluidTemplateObject(string $filename = 'Main.html'): StandaloneView
578 {
579 /** @var StandaloneView $view */
580 $view = GeneralUtility::makeInstance(StandaloneView::class);
581 $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/NewContentElement/' . $filename));
582 $view->getRequest()->setControllerExtensionName('Backend');
583 return $view;
584 }
585 }