Revert "[TASK] Avoid slow array functions in loops"
[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 * @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
26 */
27 class 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 }