[BUGFIX] Do not provide non selectable columns in colPos selector
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / View / BackendLayoutView.php
1 <?php
2 namespace TYPO3\CMS\Backend\View;
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\Core\Utility\ArrayUtility;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20
21 /**
22 * Backend layout for CMS
23 */
24 class BackendLayoutView implements \TYPO3\CMS\Core\SingletonInterface
25 {
26 /**
27 * @var BackendLayout\DataProviderCollection
28 */
29 protected $dataProviderCollection;
30
31 /**
32 * @var array
33 */
34 protected $selectedCombinedIdentifier = [];
35
36 /**
37 * @var array
38 */
39 protected $selectedBackendLayout = [];
40
41 /**
42 * Creates this object and initializes data providers.
43 */
44 public function __construct()
45 {
46 $this->initializeDataProviderCollection();
47 }
48
49 /**
50 * Initializes data providers
51 *
52 * @return void
53 */
54 protected function initializeDataProviderCollection()
55 {
56 /** @var $dataProviderCollection BackendLayout\DataProviderCollection */
57 $dataProviderCollection = GeneralUtility::makeInstance(
58 BackendLayout\DataProviderCollection::class
59 );
60
61 $dataProviderCollection->add(
62 'default',
63 \TYPO3\CMS\Backend\View\BackendLayout\DefaultDataProvider::class
64 );
65
66 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'])) {
67 $dataProviders = (array)$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'];
68 foreach ($dataProviders as $identifier => $className) {
69 $dataProviderCollection->add($identifier, $className);
70 }
71 }
72
73 $this->setDataProviderCollection($dataProviderCollection);
74 }
75
76 /**
77 * @param BackendLayout\DataProviderCollection $dataProviderCollection
78 */
79 public function setDataProviderCollection(BackendLayout\DataProviderCollection $dataProviderCollection)
80 {
81 $this->dataProviderCollection = $dataProviderCollection;
82 }
83
84 /**
85 * @return BackendLayout\DataProviderCollection
86 */
87 public function getDataProviderCollection()
88 {
89 return $this->dataProviderCollection;
90 }
91
92 /**
93 * Gets backend layout items to be shown in the forms engine.
94 * This method is called as "itemsProcFunc" with the accordant context
95 * for pages.backend_layout and pages.backend_layout_next_level.
96 *
97 * @param array $parameters
98 */
99 public function addBackendLayoutItems(array $parameters)
100 {
101 $pageId = $this->determinePageId($parameters['table'], $parameters['row']);
102 $pageTsConfig = (array)BackendUtility::getPagesTSconfig($pageId);
103 $identifiersToBeExcluded = $this->getIdentifiersToBeExcluded($pageTsConfig);
104
105 $dataProviderContext = $this->createDataProviderContext()
106 ->setPageId($pageId)
107 ->setData($parameters['row'])
108 ->setTableName($parameters['table'])
109 ->setFieldName($parameters['field'])
110 ->setPageTsConfig($pageTsConfig);
111
112 $backendLayoutCollections = $this->getDataProviderCollection()->getBackendLayoutCollections($dataProviderContext);
113 foreach ($backendLayoutCollections as $backendLayoutCollection) {
114 $combinedIdentifierPrefix = '';
115 if ($backendLayoutCollection->getIdentifier() !== 'default') {
116 $combinedIdentifierPrefix = $backendLayoutCollection->getIdentifier() . '__';
117 }
118
119 foreach ($backendLayoutCollection->getAll() as $backendLayout) {
120 $combinedIdentifier = $combinedIdentifierPrefix . $backendLayout->getIdentifier();
121
122 if (in_array($combinedIdentifier, $identifiersToBeExcluded, true)) {
123 continue;
124 }
125
126 $parameters['items'][] = [
127 $this->getLanguageService()->sL($backendLayout->getTitle()),
128 $combinedIdentifier,
129 $backendLayout->getIconPath(),
130 ];
131 }
132 }
133 }
134
135 /**
136 * Determines the page id for a given record of a database table.
137 *
138 * @param string $tableName
139 * @param array $data
140 * @return NULL|int
141 */
142 protected function determinePageId($tableName, array $data)
143 {
144 $pageId = null;
145
146 if (strpos($data['uid'], 'NEW') === 0) {
147 // negative uid_pid values of content elements indicate that the element has been inserted after an existing element
148 // so there is no pid to get the backendLayout for and we have to get that first
149 if ($data['pid'] < 0) {
150 $existingElement = $this->getDatabaseConnection()->exec_SELECTgetSingleRow(
151 'pid', $tableName, 'uid=' . abs($data['pid'])
152 );
153 if ($existingElement !== null) {
154 $pageId = $existingElement['pid'];
155 }
156 } else {
157 $pageId = $data['pid'];
158 }
159 } elseif ($tableName === 'pages') {
160 $pageId = $data['uid'];
161 } else {
162 $pageId = $data['pid'];
163 }
164
165 return $pageId;
166 }
167
168 /**
169 * Returns the backend layout which should be used for this page.
170 *
171 * @param int $pageId
172 * @return bool|string Identifier of the backend layout to be used, or FALSE if none
173 */
174 public function getSelectedCombinedIdentifier($pageId)
175 {
176 if (!isset($this->selectedCombinedIdentifier[$pageId])) {
177 $page = $this->getPage($pageId);
178 $this->selectedCombinedIdentifier[$pageId] = (string)$page['backend_layout'];
179
180 if ($this->selectedCombinedIdentifier[$pageId] === '-1') {
181 // If it is set to "none" - don't use any
182 $this->selectedCombinedIdentifier[$pageId] = false;
183 } elseif ($this->selectedCombinedIdentifier[$pageId] === '' || $this->selectedCombinedIdentifier[$pageId] === '0') {
184 // If it not set check the root-line for a layout on next level and use this
185 // (root-line starts with current page and has page "0" at the end)
186 $rootLine = $this->getRootLine($pageId);
187 // Remove first and last element (current and root page)
188 array_shift($rootLine);
189 array_pop($rootLine);
190 foreach ($rootLine as $rootLinePage) {
191 $this->selectedCombinedIdentifier[$pageId] = (string)$rootLinePage['backend_layout_next_level'];
192 if ($this->selectedCombinedIdentifier[$pageId] === '-1') {
193 // If layout for "next level" is set to "none" - don't use any and stop searching
194 $this->selectedCombinedIdentifier[$pageId] = false;
195 break;
196 } elseif ($this->selectedCombinedIdentifier[$pageId] !== '' && $this->selectedCombinedIdentifier[$pageId] !== '0') {
197 // Stop searching if a layout for "next level" is set
198 break;
199 }
200 }
201 }
202 }
203 // If it is set to a positive value use this
204 return $this->selectedCombinedIdentifier[$pageId];
205 }
206
207 /**
208 * Gets backend layout identifiers to be excluded
209 *
210 * @param array $pageTSconfig
211 * @return array
212 */
213 protected function getIdentifiersToBeExcluded(array $pageTSconfig)
214 {
215 $identifiersToBeExcluded = [];
216
217 if (ArrayUtility::isValidPath($pageTSconfig, 'options./backendLayout./exclude')) {
218 $identifiersToBeExcluded = GeneralUtility::trimExplode(
219 ',',
220 ArrayUtility::getValueByPath($pageTSconfig, 'options./backendLayout./exclude'),
221 true
222 );
223 }
224
225 return $identifiersToBeExcluded;
226 }
227
228 /**
229 * Gets colPos items to be shown in the forms engine.
230 * This method is called as "itemsProcFunc" with the accordant context
231 * for tt_content.colPos.
232 *
233 * @param array $parameters
234 * @return void
235 */
236 public function colPosListItemProcFunc(array $parameters)
237 {
238 $pageId = $this->determinePageId($parameters['table'], $parameters['row']);
239
240 if ($pageId !== null) {
241 $parameters['items'] = $this->addColPosListLayoutItems($pageId, $parameters['items']);
242 }
243 }
244
245 /**
246 * Adds items to a colpos list
247 *
248 * @param int $pageId
249 * @param array $items
250 * @return array
251 */
252 protected function addColPosListLayoutItems($pageId, $items)
253 {
254 $layout = $this->getSelectedBackendLayout($pageId);
255 if ($layout && $layout['__items']) {
256 $items = $layout['__items'];
257 }
258 return $items;
259 }
260
261 /**
262 * Gets the list of available columns for a given page id
263 *
264 * @param int $id
265 * @return array $tcaItems
266 */
267 public function getColPosListItemsParsed($id)
268 {
269 $tsConfig = BackendUtility::getModTSconfig($id, 'TCEFORM.tt_content.colPos');
270 $tcaConfig = $GLOBALS['TCA']['tt_content']['columns']['colPos']['config'];
271 $tcaItems = $tcaConfig['items'];
272 $tcaItems = $this->addItems($tcaItems, $tsConfig['properties']['addItems.']);
273 if (isset($tcaConfig['itemsProcFunc']) && $tcaConfig['itemsProcFunc']) {
274 $tcaItems = $this->addColPosListLayoutItems($id, $tcaItems);
275 }
276 foreach (GeneralUtility::trimExplode(',', $tsConfig['properties']['removeItems'], true) as $removeId) {
277 foreach ($tcaItems as $key => $item) {
278 if ($item[1] == $removeId) {
279 unset($tcaItems[$key]);
280 }
281 }
282 }
283 return $tcaItems;
284 }
285
286 /**
287 * Merges items into an item-array, optionally with an icon
288 * example:
289 * TCEFORM.pages.doktype.addItems.13 = My Label
290 * TCEFORM.pages.doktype.addItems.13.icon = EXT:t3skin/icons/gfx/i/pages.gif
291 *
292 * @param array $items The existing item array
293 * @param array $iArray An array of items to add. NOTICE: The keys are mapped to values, and the values and mapped to be labels. No possibility of adding an icon.
294 * @return array The updated $item array
295 * @internal
296 */
297 protected function addItems($items, $iArray)
298 {
299 $languageService = static::getLanguageService();
300 if (is_array($iArray)) {
301 foreach ($iArray as $value => $label) {
302 // if the label is an array (that means it is a subelement
303 // like "34.icon = mylabel.png", skip it (see its usage below)
304 if (is_array($label)) {
305 continue;
306 }
307 // check if the value "34 = mylabel" also has a "34.icon = myimage.png"
308 if (isset($iArray[$value . '.']) && $iArray[$value . '.']['icon']) {
309 $icon = $iArray[$value . '.']['icon'];
310 } else {
311 $icon = '';
312 }
313 $items[] = [$languageService->sL($label), $value, $icon];
314 }
315 }
316 return $items;
317 }
318
319 /**
320 * Gets the selected backend layout
321 *
322 * @param int $pageId
323 * @return array|NULL $backendLayout
324 */
325 public function getSelectedBackendLayout($pageId)
326 {
327 if (isset($this->selectedBackendLayout[$pageId])) {
328 return $this->selectedBackendLayout[$pageId];
329 }
330 $backendLayoutData = null;
331
332 $selectedCombinedIdentifier = $this->getSelectedCombinedIdentifier($pageId);
333 // If no backend layout is selected, use default
334 if (empty($selectedCombinedIdentifier)) {
335 $selectedCombinedIdentifier = 'default';
336 }
337
338 $backendLayout = $this->getDataProviderCollection()->getBackendLayout($selectedCombinedIdentifier, $pageId);
339 // If backend layout is not found available anymore, use default
340 if (is_null($backendLayout)) {
341 $selectedCombinedIdentifier = 'default';
342 $backendLayout = $this->getDataProviderCollection()->getBackendLayout($selectedCombinedIdentifier, $pageId);
343 }
344
345 if (!empty($backendLayout)) {
346 /** @var $parser \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser */
347 $parser = GeneralUtility::makeInstance(\TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::class);
348 /** @var \TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher $conditionMatcher */
349 $conditionMatcher = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher::class);
350 $parser->parse($parser->checkIncludeLines($backendLayout->getConfiguration()), $conditionMatcher);
351
352 $backendLayoutData = [];
353 $backendLayoutData['config'] = $backendLayout->getConfiguration();
354 $backendLayoutData['__config'] = $parser->setup;
355 $backendLayoutData['__items'] = [];
356 $backendLayoutData['__colPosList'] = [];
357
358 // create items and colPosList
359 if (!empty($backendLayoutData['__config']['backend_layout.']['rows.'])) {
360 foreach ($backendLayoutData['__config']['backend_layout.']['rows.'] as $row) {
361 if (!empty($row['columns.'])) {
362 foreach ($row['columns.'] as $column) {
363 if (!isset($column['colPos'])) {
364 continue;
365 }
366 $backendLayoutData['__items'][] = [
367 GeneralUtility::isFirstPartOfStr($column['name'], 'LLL:') ? $this->getLanguageService()->sL($column['name']) : $column['name'],
368 $column['colPos'],
369 null
370 ];
371 $backendLayoutData['__colPosList'][] = $column['colPos'];
372 }
373 }
374 }
375 }
376
377 $this->selectedBackendLayout[$pageId] = $backendLayoutData;
378 }
379
380 return $backendLayoutData;
381 }
382
383 /**
384 * Get default columns layout
385 *
386 * @return string Default four column layout
387 * @static
388 */
389 public static function getDefaultColumnLayout()
390 {
391 return '
392 backend_layout {
393 colCount = 4
394 rowCount = 1
395 rows {
396 1 {
397 columns {
398 1 {
399 name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.0
400 colPos = 1
401 }
402 2 {
403 name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.1
404 colPos = 0
405 }
406 3 {
407 name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.2
408 colPos = 2
409 }
410 4 {
411 name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.3
412 colPos = 3
413 }
414 }
415 }
416 }
417 }
418 ';
419 }
420
421 /**
422 * Gets a page record.
423 *
424 * @param int $pageId
425 * @return NULL|array
426 */
427 protected function getPage($pageId)
428 {
429 $page = $this->getDatabaseConnection()->exec_SELECTgetSingleRow(
430 'uid, pid, backend_layout',
431 'pages',
432 'uid=' . (int)$pageId
433 );
434 BackendUtility::workspaceOL('pages', $page);
435 return $page;
436 }
437
438 /**
439 * Gets the page root-line.
440 *
441 * @param int $pageId
442 * @return array
443 */
444 protected function getRootLine($pageId)
445 {
446 return BackendUtility::BEgetRootLine($pageId, '', true);
447 }
448
449 /**
450 * @return BackendLayout\DataProviderContext
451 */
452 protected function createDataProviderContext()
453 {
454 return GeneralUtility::makeInstance(BackendLayout\DataProviderContext::class);
455 }
456
457 /**
458 * @return \TYPO3\CMS\Core\Database\DatabaseConnection
459 */
460 protected function getDatabaseConnection()
461 {
462 return $GLOBALS['TYPO3_DB'];
463 }
464
465 /**
466 * @return \TYPO3\CMS\Lang\LanguageService
467 */
468 protected function getLanguageService()
469 {
470 return $GLOBALS['LANG'];
471 }
472 }