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