[BUGFIX] Fix column layout for New Content Element wizard
[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 TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Backend\Utility\IconUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20
21 /**
22 * Script Class for the New Content element wizard
23 *
24 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
25 */
26 class NewContentElementController {
27
28 // Internal, static (from GPvars):
29 // Page id
30 /**
31 * @var int
32 */
33 public $id;
34
35 // Sys language
36 /**
37 * @var int
38 */
39 public $sys_language = 0;
40
41 // Return URL.
42 /**
43 * @var string
44 */
45 public $R_URI = '';
46
47 // If set, the content is destined for a specific column.
48 /**
49 * @var int|null
50 */
51 public $colPos;
52
53 /**
54 * @var int
55 */
56 public $uid_pid;
57
58 // Internal, static:
59 // Module TSconfig.
60 /**
61 * @var array
62 */
63 public $modTSconfig = array();
64
65 /**
66 * Internal backend template object
67 *
68 * @var \TYPO3\CMS\Backend\Template\DocumentTemplate
69 */
70 public $doc;
71
72 // Internal, dynamic:
73 // Includes a list of files to include between init() and main() - see init()
74 /**
75 * @var array
76 */
77 public $include_once = array();
78
79 // Used to accumulate the content of the module.
80 /**
81 * @var string
82 */
83 public $content;
84
85 // Access boolean.
86 /**
87 * @var bool
88 */
89 public $access;
90
91 // config of the wizard
92 /**
93 * @var array
94 */
95 public $config;
96
97 /**
98 * Constructor, initializing internal variables.
99 *
100 * @return void
101 */
102 public function init() {
103 // Setting class files to include:
104 if (is_array($GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses'])) {
105 $this->include_once = array_merge($this->include_once, $GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses']);
106 }
107 // Setting internal vars:
108 $this->id = (int)GeneralUtility::_GP('id');
109 $this->sys_language = (int)GeneralUtility::_GP('sys_language_uid');
110 $this->R_URI = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
111 $this->colPos = GeneralUtility::_GP('colPos') === NULL ? NULL : (int)GeneralUtility::_GP('colPos');
112 $this->uid_pid = (int)GeneralUtility::_GP('uid_pid');
113 $this->MCONF['name'] = 'xMOD_db_new_content_el';
114 $this->modTSconfig = BackendUtility::getModTSconfig($this->id, 'mod.wizards.newContentElement');
115 $config = BackendUtility::getPagesTSconfig($this->id);
116 $this->config = $config['mod.']['wizards.']['newContentElement.'];
117 // Starting the document template object:
118 $this->doc = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Template\\DocumentTemplate');
119 $this->doc->backPath = $GLOBALS['BACK_PATH'];
120 $this->doc->setModuleTemplate('EXT:backend/Resources/Private/Templates/db_new_content_el.html');
121 $this->doc->JScode = '';
122 $this->doc->form = '<form action="" name="editForm"><input type="hidden" name="defValues" value="" />';
123 // Setting up the context sensitive menu:
124 $this->doc->getContextMenuCode();
125 // Getting the current page and receiving access information (used in main())
126 $perms_clause = $GLOBALS['BE_USER']->getPagePermsClause(1);
127 $this->pageinfo = BackendUtility::readPageAccess($this->id, $perms_clause);
128 $this->access = is_array($this->pageinfo) ? 1 : 0;
129 }
130
131 /**
132 * Creating the module output.
133 *
134 * @return void
135 */
136 public function main() {
137 if ($this->id && $this->access) {
138 // Init position map object:
139 $posMap = GeneralUtility::makeInstance('ext_posMap');
140 $posMap->cur_sys_language = $this->sys_language;
141 $posMap->backPath = $GLOBALS['BACK_PATH'];
142 // If a column is pre-set:
143 if (isset($this->colPos)) {
144 if ($this->uid_pid < 0) {
145 $row = array();
146 $row['uid'] = abs($this->uid_pid);
147 } else {
148 $row = '';
149 }
150 $this->onClickEvent = $posMap->onClickInsertRecord($row, $this->colPos, '', $this->uid_pid, $this->sys_language);
151 } else {
152 $this->onClickEvent = '';
153 }
154 // ***************************
155 // Creating content
156 // ***************************
157 // use a wrapper div
158 $this->content .= '<div id="user-setup-wrapper">';
159 $this->content .= $this->doc->header($GLOBALS['LANG']->getLL('newContentElement'));
160 $this->content .= $this->doc->spacer(5);
161 // Wizard
162 $code = '';
163 $wizardItems = $this->getWizardItems();
164 // Wrapper for wizards
165 $this->elementWrapper['section'] = array('<ul class="contentelement-wizard list-unstyled">', '</ul>');
166 // Copy wrapper for tabs
167 $this->elementWrapperForTabs = $this->elementWrapper;
168 // Hook for manipulating wizardItems, wrapper, onClickEvent etc.
169 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms']['db_new_content_el']['wizardItemsHook'])) {
170 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms']['db_new_content_el']['wizardItemsHook'] as $classData) {
171 $hookObject = GeneralUtility::getUserObj($classData);
172 if (!$hookObject instanceof \TYPO3\CMS\Backend\Wizard\NewContentElementWizardHookInterface) {
173 throw new \UnexpectedValueException('$hookObject must implement interface TYPO3\\CMS\\Backend\\Wizard\\NewContentElementWizardHookInterface', 1227834741);
174 }
175 $hookObject->manipulateWizardItems($wizardItems, $this);
176 }
177 }
178 if ($this->config['renderMode'] == 'tabs' && $this->elementWrapperForTabs != $this->elementWrapper) {
179 // Restore wrapper for tabs if they are overwritten in hook
180 $this->elementWrapper = $this->elementWrapperForTabs;
181 }
182 // Add document inline javascript
183 $this->doc->JScode = $this->doc->wrapScriptTags('
184 function goToalt_doc() { //
185 ' . $this->onClickEvent . '
186 }
187
188 if(top.refreshMenu) {
189 top.refreshMenu();
190 } else {
191 top.TYPO3ModuleMenu.refreshMenu();
192 }
193 ');
194 // Traverse items for the wizard.
195 // An item is either a header or an item rendered with a radio button and title/description and icon:
196 $cc = ($key = 0);
197 $menuItems = array();
198 foreach ($wizardItems as $k => $wInfo) {
199 if ($wInfo['header']) {
200 $menuItems[] = array(
201 'label' => htmlspecialchars($wInfo['header']),
202 'content' => $this->elementWrapper['section'][0]
203 );
204 $key = count($menuItems) - 1;
205 } else {
206 $content = '';
207
208 if (!$this->onClickEvent) {
209 // Radio button:
210 $oC = 'document.editForm.defValues.value=unescape(\'' . rawurlencode($wInfo['params']) . '\');goToalt_doc();' . (!$this->onClickEvent ? 'window.location.hash=\'#sel2\';' : '');
211 $content .= '<div class="contentelement-wizard-item-input"><input type="radio" name="tempB" value="' . htmlspecialchars($k) . '" onclick="' . htmlspecialchars($oC) . '" /></div>';
212 // Onclick action for icon/title:
213 $aOnClick = 'document.getElementsByName(\'tempB\')[' . $cc . '].checked=1;' . $oC . 'return false;';
214 } else {
215 $aOnClick = "document.editForm.defValues.value=unescape('" . rawurlencode($wInfo['params']) . "');goToalt_doc();" . (!$this->onClickEvent?"window.location.hash='#sel2';":'');
216 }
217
218 $menuItems[$key]['content'] .=
219 '<li>
220 <div class="contentelement-wizard-item">
221 ' . $content . '
222 <div class="contentelement-wizard-item-icon">
223 <a href="#" onclick="' . htmlspecialchars($aOnClick) . '">
224 <img' . IconUtility::skinImg($this->doc->backPath, $wInfo['icon'], '') . ' alt="" />
225 </a>
226 </div>
227 <div class="contentelement-wizard-item-text">
228 <a href="#" onclick="' . htmlspecialchars($aOnClick) . '">
229 <strong>' . htmlspecialchars($wInfo['title']) . '</strong>
230 <br />' . nl2br(htmlspecialchars(trim($wInfo['description']))) .
231 '</a>
232 </div>
233 </div>
234 </li>';
235 $cc++;
236 }
237 }
238 // Add closing section-tag
239 foreach ($menuItems as $key => $val) {
240 $menuItems[$key]['content'] .= $this->elementWrapper['section'][1];
241 }
242 // Add the wizard table to the content, wrapped in tabs:
243 if ($this->config['renderMode'] == 'tabs') {
244 $code = $GLOBALS['LANG']->getLL('sel1', 1) . '<br /><br />' . $this->doc->getDynTabMenu($menuItems, 'new-content-element-wizard', FALSE, FALSE);
245 } else {
246 $code = $GLOBALS['LANG']->getLL('sel1', 1) . '<br /><br />';
247 foreach ($menuItems as $section) {
248 $code .= '<h3 class="divider">' . $section['label'] . '</h3>' . $section['content'];
249 }
250 }
251 $this->content .= $this->doc->section(!$this->onClickEvent ? $GLOBALS['LANG']->getLL('1_selectType') : '', $code, 0, 1);
252 // If the user must also select a column:
253 if (!$this->onClickEvent) {
254 // Add anchor "sel2"
255 $this->content .= $this->doc->section('', '<a name="sel2"></a>');
256 $this->content .= $this->doc->spacer(20);
257 // Select position
258 $code = $GLOBALS['LANG']->getLL('sel2', 1) . '<br /><br />';
259
260 // Load SHARED page-TSconfig settings and retrieve column list from there, if applicable:
261 $colPosArray = GeneralUtility::callUserFunction('TYPO3\\CMS\\Backend\\View\\BackendLayoutView->getColPosListItemsParsed', $this->id, $this);
262 $colPosIds = array_column($colPosArray, 1);
263 // Removing duplicates, if any
264 $colPosList = implode(',', array_unique(array_map('intval', $colPosIds)));
265 // Finally, add the content of the column selector to the content:
266 $code .= $posMap->printContentElementColumns($this->id, 0, $colPosList, 1, $this->R_URI);
267 $this->content .= $this->doc->section($GLOBALS['LANG']->getLL('2_selectPosition'), $code, 0, 1);
268 }
269 // Close wrapper div
270 $this->content .= '</div>';
271 } else {
272 // In case of no access:
273 $this->content = '';
274 $this->content .= $this->doc->header($GLOBALS['LANG']->getLL('newContentElement'));
275 $this->content .= $this->doc->spacer(5);
276 }
277 // Setting up the buttons and markers for docheader
278 $docHeaderButtons = $this->getButtons();
279 $markers['CSH'] = $docHeaderButtons['csh'];
280 $markers['CONTENT'] = $this->content;
281 // Build the <body> for the module
282 $this->content = $this->doc->startPage($GLOBALS['LANG']->getLL('newContentElement'));
283 $this->content .= $this->doc->moduleBody($this->pageinfo, $docHeaderButtons, $markers);
284 $this->content .= $this->doc->sectionEnd();
285 $this->content .= $this->doc->endPage();
286 $this->content = $this->doc->insertStylesAndJS($this->content);
287 }
288
289 /**
290 * Print out the accumulated content:
291 *
292 * @return void
293 */
294 public function printContent() {
295 echo $this->content;
296 }
297
298 /**
299 * Create the panel of buttons for submitting the form or otherwise perform operations.
300 *
301 * @return array All available buttons as an assoc. array
302 */
303 protected function getButtons() {
304 $buttons = array(
305 'csh' => '',
306 'back' => ''
307 );
308 if ($this->id && $this->access) {
309 $buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'new_ce', $GLOBALS['BACK_PATH'], '', TRUE);
310 if ($this->R_URI) {
311 $buttons['back'] = '<a href="' . htmlspecialchars($this->R_URI) . '" class="typo3-goBack" title="' . $GLOBALS['LANG']->getLL('goBack', TRUE) . '">' . IconUtility::getSpriteIcon('actions-view-go-back') . '</a>';
312 }
313 }
314 return $buttons;
315 }
316
317 /***************************
318 *
319 * OTHER FUNCTIONS:
320 *
321 ***************************/
322 /**
323 * Returns the content of wizardArray() function...
324 *
325 * @return array Returns the content of wizardArray() function...
326 */
327 public function getWizardItems() {
328 return $this->wizardArray();
329 }
330
331 /**
332 * Returns the array of elements in the wizard display.
333 * For the plugin section there is support for adding elements there from a global variable.
334 *
335 * @return array
336 */
337 public function wizardArray() {
338 if (is_array($this->config)) {
339 $wizards = $this->config['wizardItems.'];
340 }
341 $appendWizards = $this->wizard_appendWizards($wizards['elements.']);
342 $wizardItems = array();
343 if (is_array($wizards)) {
344 foreach ($wizards as $groupKey => $wizardGroup) {
345 $groupKey = preg_replace('/\\.$/', '', $groupKey);
346 $showItems = GeneralUtility::trimExplode(',', $wizardGroup['show'], TRUE);
347 $showAll = $wizardGroup['show'] === '*';
348 $groupItems = array();
349 if (is_array($appendWizards[$groupKey . '.']['elements.'])) {
350 $wizardElements = array_merge((array) $wizardGroup['elements.'], $appendWizards[$groupKey . '.']['elements.']);
351 } else {
352 $wizardElements = $wizardGroup['elements.'];
353 }
354 if (is_array($wizardElements)) {
355 foreach ($wizardElements as $itemKey => $itemConf) {
356 $itemKey = preg_replace('/\\.$/', '', $itemKey);
357 if ($showAll || in_array($itemKey, $showItems)) {
358 $tmpItem = $this->wizard_getItem($groupKey, $itemKey, $itemConf);
359 if ($tmpItem) {
360 $groupItems[$groupKey . '_' . $itemKey] = $tmpItem;
361 }
362 }
363 }
364 }
365 if (count($groupItems)) {
366 $wizardItems[$groupKey] = $this->wizard_getGroupHeader($groupKey, $wizardGroup);
367 $wizardItems = array_merge($wizardItems, $groupItems);
368 }
369 }
370 }
371 // Remove elements where preset values are not allowed:
372 $this->removeInvalidElements($wizardItems);
373 return $wizardItems;
374 }
375
376 /**
377 * @param mixed $wizardElements
378 * @return array
379 */
380 public function wizard_appendWizards($wizardElements) {
381 if (!is_array($wizardElements)) {
382 $wizardElements = array();
383 }
384 if (is_array($GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses'])) {
385 foreach ($GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses'] as $class => $path) {
386 require_once $path;
387 $modObj = GeneralUtility::makeInstance($class);
388 $wizardElements = $modObj->proc($wizardElements);
389 }
390 }
391 $returnElements = array();
392 foreach ($wizardElements as $key => $wizardItem) {
393 preg_match('/^[a-zA-Z0-9]+_/', $key, $group);
394 $wizardGroup = $group[0] ? substr($group[0], 0, -1) . '.' : $key;
395 $returnElements[$wizardGroup]['elements.'][substr($key, strlen($wizardGroup)) . '.'] = $wizardItem;
396 }
397 return $returnElements;
398 }
399
400 /**
401 * @param string Not used
402 * @param string Not used
403 * @param array $itemConf
404 * @return array
405 */
406 public function wizard_getItem($groupKey, $itemKey, $itemConf) {
407 $itemConf['title'] = $GLOBALS['LANG']->sL($itemConf['title']);
408 $itemConf['description'] = $GLOBALS['LANG']->sL($itemConf['description']);
409 $itemConf['tt_content_defValues'] = $itemConf['tt_content_defValues.'];
410 unset($itemConf['tt_content_defValues.']);
411 return $itemConf;
412 }
413
414 /**
415 * @param string Not used
416 * @param array $wizardGroup
417 * @return array
418 */
419 public function wizard_getGroupHeader($groupKey, $wizardGroup) {
420 return array(
421 'header' => $GLOBALS['LANG']->sL($wizardGroup['header'])
422 );
423 }
424
425 /**
426 * Checks the array for elements which might contain unallowed default values and will unset them!
427 * Looks for the "tt_content_defValues" key in each element and if found it will traverse that array as fieldname / value pairs and check. The values will be added to the "params" key of the array (which should probably be unset or empty by default).
428 *
429 * @param array $wizardItems Wizard items, passed by reference
430 * @return void
431 */
432 public function removeInvalidElements(&$wizardItems) {
433 // Get TCEFORM from TSconfig of current page
434 $row = array('pid' => $this->id);
435 $TCEFORM_TSconfig = BackendUtility::getTCEFORM_TSconfig('tt_content', $row);
436 $headersUsed = array();
437 // Traverse wizard items:
438 foreach ($wizardItems as $key => $cfg) {
439 // Exploding parameter string, if any (old style)
440 if ($wizardItems[$key]['params']) {
441 // Explode GET vars recursively
442 $tempGetVars = GeneralUtility::explodeUrl2Array($wizardItems[$key]['params'], TRUE);
443 // If tt_content values are set, merge them into the tt_content_defValues array,
444 // unset them from $tempGetVars and re-implode $tempGetVars into the param string
445 // (in case remaining parameters are around).
446 if (is_array($tempGetVars['defVals']['tt_content'])) {
447 $wizardItems[$key]['tt_content_defValues'] = array_merge(
448 is_array($wizardItems[$key]['tt_content_defValues'])
449 ? $wizardItems[$key]['tt_content_defValues']
450 : array(),
451 $tempGetVars['defVals']['tt_content']
452 );
453 unset($tempGetVars['defVals']['tt_content']);
454 $wizardItems[$key]['params'] = GeneralUtility::implodeArrayForUrl('', $tempGetVars);
455 }
456 }
457 // If tt_content_defValues are defined...:
458 if (is_array($wizardItems[$key]['tt_content_defValues'])) {
459 // Traverse field values:
460 foreach ($wizardItems[$key]['tt_content_defValues'] as $fN => $fV) {
461 if (is_array($GLOBALS['TCA']['tt_content']['columns'][$fN])) {
462 // Get information about if the field value is OK:
463 $config = &$GLOBALS['TCA']['tt_content']['columns'][$fN]['config'];
464 $authModeDeny = $config['type'] == 'select' && $config['authMode']
465 && !$GLOBALS['BE_USER']->checkAuthMode('tt_content', $fN, $fV, $config['authMode']);
466 // explode TSconfig keys only as needed
467 if (!isset($removeItems[$fN])) {
468 $removeItems[$fN] = GeneralUtility::trimExplode(',', $TCEFORM_TSconfig[$fN]['removeItems'], TRUE);
469 }
470 if (!isset($keepItems[$fN])) {
471 $keepItems[$fN] = GeneralUtility::trimExplode(',', $TCEFORM_TSconfig[$fN]['keepItems'], TRUE);
472 }
473 $isNotInKeepItems = count($keepItems[$fN]) && !in_array($fV, $keepItems[$fN]);
474 if ($authModeDeny || in_array($fV, $removeItems[$fN]) || $isNotInKeepItems) {
475 // Remove element all together:
476 unset($wizardItems[$key]);
477 break;
478 } else {
479 // Add the parameter:
480 $wizardItems[$key]['params'] .= '&defVals[tt_content][' . $fN . ']=' . rawurlencode($fV);
481 $tmp = explode('_', $key);
482 $headersUsed[$tmp[0]] = $tmp[0];
483 }
484 }
485 }
486 }
487 }
488 // remove headers without elements
489 foreach ($wizardItems as $key => $cfg) {
490 $tmp = explode('_', $key);
491 if ($tmp[0] && !$tmp[1] && !in_array($tmp[0], $headersUsed)) {
492 unset($wizardItems[$key]);
493 }
494 }
495 }
496
497 }