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