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