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