Completed
Push — master ( 17ab80...0e464a )
by
unknown
26:32 queued 09:43
created

BackendLayoutView::addBackendLayoutItems()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 22
nc 7
nop 1
dl 0
loc 31
rs 9.2568
c 0
b 0
f 0
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\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
18
use TYPO3\CMS\Backend\Utility\BackendUtility;
19
use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
20
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderCollection;
21
use TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext;
22
use TYPO3\CMS\Backend\View\BackendLayout\DefaultDataProvider;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\Localization\LanguageService;
25
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
26
use TYPO3\CMS\Core\Utility\ArrayUtility;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28
29
/**
30
 * Backend layout for CMS
31
 * @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
32
 */
33
class BackendLayoutView implements \TYPO3\CMS\Core\SingletonInterface
34
{
35
    /**
36
     * @var DataProviderCollection
37
     */
38
    protected $dataProviderCollection;
39
40
    /**
41
     * @var array
42
     */
43
    protected $selectedCombinedIdentifier = [];
44
45
    /**
46
     * @var array
47
     */
48
    protected $selectedBackendLayout = [];
49
50
    /**
51
     * Creates this object and initializes data providers.
52
     */
53
    public function __construct()
54
    {
55
        $this->initializeDataProviderCollection();
56
    }
57
58
    /**
59
     * Initializes data providers
60
     */
61
    protected function initializeDataProviderCollection()
62
    {
63
        $dataProviderCollection = GeneralUtility::makeInstance(DataProviderCollection::class);
64
        $dataProviderCollection->add('default', DefaultDataProvider::class);
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 DataProviderCollection $dataProviderCollection
78
     */
79
    public function setDataProviderCollection(DataProviderCollection $dataProviderCollection)
80
    {
81
        $this->dataProviderCollection = $dataProviderCollection;
82
    }
83
84
    /**
85
     * @return 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);
0 ignored issues
show
Bug introduced by
It seems like $pageId can also be of type boolean; however, parameter $id of TYPO3\CMS\Backend\Utilit...ity::getPagesTSconfig() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

102
        $pageTsConfig = (array)BackendUtility::getPagesTSconfig(/** @scrutinizer ignore-type */ $pageId);
Loading history...
103
        $identifiersToBeExcluded = $this->getIdentifiersToBeExcluded($pageTsConfig);
104
105
        $dataProviderContext = $this->createDataProviderContext()
106
            ->setPageId($pageId)
0 ignored issues
show
Bug introduced by
It seems like $pageId can also be of type boolean; however, parameter $pageId of TYPO3\CMS\Backend\View\B...derContext::setPageId() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

106
            ->setPageId(/** @scrutinizer ignore-type */ $pageId)
Loading history...
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']);
0 ignored issues
show
Bug introduced by
It seems like $pageId can also be of type true; however, parameter $pageId of TYPO3\CMS\Backend\View\B...ColPosListLayoutItems() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

249
            $parameters['items'] = $this->addColPosListLayoutItems(/** @scrutinizer ignore-type */ $pageId, $parameters['items']);
Loading history...
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']) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $layout of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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();
0 ignored issues
show
Bug Best Practice introduced by
The method TYPO3\CMS\Backend\View\B...w::getLanguageService() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

309
        /** @scrutinizer ignore-call */ 
310
        $languageService = static::getLanguageService();
Loading history...
310
        if (is_array($iArray)) {
0 ignored issues
show
introduced by
The condition is_array($iArray) is always true.
Loading history...
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 structure as an array
331
     *
332
     * @param int $pageId
333
     * @return array|null $backendLayout
334
     */
335
    public function getSelectedBackendLayout($pageId): ?array
336
    {
337
        $layout = $this->getBackendLayoutForPage((int)$pageId);
338
        if ($layout instanceof BackendLayout) {
0 ignored issues
show
introduced by
$layout is always a sub-type of TYPO3\CMS\Backend\View\BackendLayout\BackendLayout.
Loading history...
339
            return $layout->getStructure();
340
        }
341
        return null;
342
    }
343
344
    /**
345
     * Get the BackendLayout object and parse the structure based on the UserTSconfig
346
     * @param int $pageId
347
     * @return BackendLayout
348
     */
349
    public function getBackendLayoutForPage(int $pageId): ?BackendLayout
350
    {
351
        if (isset($this->selectedBackendLayout[$pageId])) {
352
            return $this->selectedBackendLayout[$pageId];
353
        }
354
        $selectedCombinedIdentifier = $this->getSelectedCombinedIdentifier($pageId);
355
        // If no backend layout is selected, use default
356
        if (empty($selectedCombinedIdentifier)) {
357
            $selectedCombinedIdentifier = 'default';
358
        }
359
        $backendLayout = $this->getDataProviderCollection()->getBackendLayout($selectedCombinedIdentifier, $pageId);
360
        // If backend layout is not found available anymore, use default
361
        if ($backendLayout === null) {
362
            $backendLayout = $this->getDataProviderCollection()->getBackendLayout('default', $pageId);
363
        }
364
365
        $structure = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $structure is dead and can be removed.
Loading history...
366
        if ($backendLayout instanceof BackendLayout) {
367
            $structure = $this->parseStructure($backendLayout);
368
            // Parse the configuration and inject it back in the backend layout object
369
            $backendLayout->setStructure($structure);
370
            $this->selectedBackendLayout[$pageId] = $backendLayout;
371
        }
372
        return $backendLayout;
373
    }
374
375
    /**
376
     * @param BackendLayout $backendLayout
377
     * @return array
378
     * @internal
379
     */
380
    public function parseStructure(BackendLayout $backendLayout): array
381
    {
382
        $parser = GeneralUtility::makeInstance(TypoScriptParser::class);
383
        $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
384
        $parser->parse(TypoScriptParser::checkIncludeLines($backendLayout->getConfiguration()), $conditionMatcher);
385
386
        $backendLayoutData = [];
387
        $backendLayoutData['config'] = $backendLayout->getConfiguration();
388
        $backendLayoutData['__config'] = $parser->setup;
389
        $backendLayoutData['__items'] = [];
390
        $backendLayoutData['__colPosList'] = [];
391
        $backendLayoutData['usedColumns'] = [];
392
393
        // create items and colPosList
394
        if (!empty($backendLayoutData['__config']['backend_layout.']['rows.'])) {
395
            foreach ($backendLayoutData['__config']['backend_layout.']['rows.'] as $row) {
396
                if (!empty($row['columns.'])) {
397
                    foreach ($row['columns.'] as $column) {
398
                        if (!isset($column['colPos'])) {
399
                            continue;
400
                        }
401
                        $backendLayoutData['__items'][] = [
402
                            $this->getColumnName($column),
403
                            $column['colPos'],
404
                            null
405
                        ];
406
                        $backendLayoutData['__colPosList'][] = $column['colPos'];
407
                        $backendLayoutData['usedColumns'][(int)$column['colPos']] = $column['name'];
408
                    }
409
                }
410
            }
411
        }
412
        return $backendLayoutData;
413
    }
414
415
    /**
416
     * Get default columns layout
417
     *
418
     * @return string Default four column layout
419
     * @static
420
     */
421
    public static function getDefaultColumnLayout()
422
    {
423
        return '
424
		backend_layout {
425
			colCount = 1
426
			rowCount = 1
427
			rows {
428
				1 {
429
					columns {
430
						1 {
431
							name = LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:colPos.I.1
432
							colPos = 0
433
						}
434
					}
435
				}
436
			}
437
		}
438
		';
439
    }
440
441
    /**
442
     * Gets a page record.
443
     *
444
     * @param int $pageId
445
     * @return array|null
446
     */
447
    protected function getPage($pageId)
448
    {
449
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
450
            ->getQueryBuilderForTable('pages');
451
        $queryBuilder->getRestrictions()
452
            ->removeAll();
453
        $page = $queryBuilder
454
            ->select('uid', 'pid', 'backend_layout')
455
            ->from('pages')
456
            ->where(
457
                $queryBuilder->expr()->eq(
458
                    'uid',
459
                    $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
460
                )
461
            )
462
            ->execute()
463
            ->fetch();
464
        BackendUtility::workspaceOL('pages', $page);
465
466
        return $page;
467
    }
468
469
    /**
470
     * Gets the page root-line.
471
     *
472
     * @param int $pageId
473
     * @return array
474
     */
475
    protected function getRootLine($pageId)
476
    {
477
        return BackendUtility::BEgetRootLine($pageId, '', true);
478
    }
479
480
    /**
481
     * @return DataProviderContext
482
     */
483
    protected function createDataProviderContext()
484
    {
485
        return GeneralUtility::makeInstance(DataProviderContext::class);
486
    }
487
488
    /**
489
     * @return LanguageService
490
     */
491
    protected function getLanguageService()
492
    {
493
        return $GLOBALS['LANG'];
494
    }
495
496
    /**
497
     * Get column name from colPos item structure
498
     *
499
     * @param array $column
500
     * @return string
501
     */
502
    protected function getColumnName($column)
503
    {
504
        $columnName = $column['name'];
505
506
        if (GeneralUtility::isFirstPartOfStr($columnName, 'LLL:')) {
507
            $columnName = $this->getLanguageService()->sL($columnName);
508
        }
509
510
        return $columnName;
511
    }
512
}
513