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