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