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