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