Passed
Push — master ( 7ac07a...8ad661 )
by
unknown
13:02
created

AbstractItemProvider::addItemsFromForeignTable()   D

Complexity

Conditions 18
Paths 48

Size

Total Lines 94
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 57
c 1
b 0
f 0
dl 0
loc 94
rs 4.8666
cc 18
nc 48
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Backend\Form\FormDataProvider;
17
18
use Doctrine\DBAL\Exception as DBALException;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
23
use TYPO3\CMS\Core\Database\Query\QueryHelper;
24
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
25
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
26
use TYPO3\CMS\Core\Database\RelationHandler;
27
use TYPO3\CMS\Core\Hooks\TcaItemsProcessorFunctions;
28
use TYPO3\CMS\Core\Imaging\IconFactory;
29
use TYPO3\CMS\Core\Localization\LanguageService;
30
use TYPO3\CMS\Core\Messaging\FlashMessage;
31
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
32
use TYPO3\CMS\Core\Messaging\FlashMessageService;
33
use TYPO3\CMS\Core\Resource\FileRepository;
34
use TYPO3\CMS\Core\Resource\ResourceStorage;
35
use TYPO3\CMS\Core\Type\Bitmask\Permission;
36
use TYPO3\CMS\Core\Utility\ArrayUtility;
37
use TYPO3\CMS\Core\Utility\GeneralUtility;
38
use TYPO3\CMS\Core\Utility\MathUtility;
39
use TYPO3\CMS\Core\Versioning\VersionState;
40
41
/**
42
 * Contains methods used by Data providers that handle elements
43
 * with single items like select, radio and some more.
44
 */
45
abstract class AbstractItemProvider
46
{
47
    /**
48
     * Resolve "itemProcFunc" of elements.
49
     *
50
     * @param array $result Main result array
51
     * @param string $fieldName Field name to handle item list for
52
     * @param array $items Existing items array
53
     * @return array New list of item elements
54
     */
55
    protected function resolveItemProcessorFunction(array $result, $fieldName, array $items)
56
    {
57
        $table = $result['tableName'];
58
        $config = $result['processedTca']['columns'][$fieldName]['config'];
59
60
        $pageTsProcessorParameters = null;
61
        if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'])) {
62
            $pageTsProcessorParameters = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'];
63
        }
64
        $processorParameters = [
65
            // Function manipulates $items directly and return nothing
66
            'items' => &$items,
67
            'config' => $config,
68
            'TSconfig' => $pageTsProcessorParameters,
69
            'table' => $table,
70
            'row' => $result['databaseRow'],
71
            'field' => $fieldName,
72
            'inlineParentUid' => $result['inlineParentUid'],
73
            'inlineParentTableName' => $result['inlineParentTableName'],
74
            'inlineParentFieldName' => $result['inlineParentFieldName'],
75
            'inlineParentConfig' => $result['inlineParentConfig'],
76
            'inlineTopMostParentUid' => $result['inlineTopMostParentUid'],
77
            'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'],
78
            'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'],
79
        ];
80
        if (!empty($result['flexParentDatabaseRow'])) {
81
            $processorParameters['flexParentDatabaseRow'] = $result['flexParentDatabaseRow'];
82
        }
83
84
        try {
85
            GeneralUtility::callUserFunction($config['itemsProcFunc'], $processorParameters, $this);
86
        } catch (\Exception $exception) {
87
            // The itemsProcFunc method may throw an exception, create a flash message if so
88
            $languageService = $this->getLanguageService();
0 ignored issues
show
Bug introduced by
The method getLanguageService() does not exist on null. ( Ignorable by Annotation )

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

88
            /** @scrutinizer ignore-call */ 
89
            $languageService = $this->getLanguageService();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
89
            $fieldLabel = $fieldName;
90
            if (!empty($result['processedTca']['columns'][$fieldName]['label'])) {
91
                $fieldLabel = $languageService->sL($result['processedTca']['columns'][$fieldName]['label']);
92
            }
93
            $message = sprintf(
94
                $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.items_proc_func_error'),
95
                $fieldLabel,
96
                $exception->getMessage()
97
            );
98
            /** @var FlashMessage $flashMessage */
99
            $flashMessage = GeneralUtility::makeInstance(
100
                FlashMessage::class,
101
                $message,
102
                '',
103
                FlashMessage::ERROR,
104
                true
105
            );
106
            /** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
107
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
108
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
109
            $defaultFlashMessageQueue->enqueue($flashMessage);
110
        }
111
112
        return $items;
113
    }
114
115
    /**
116
     * PageTsConfig addItems:
117
     *
118
     * TCEFORMS.aTable.aField[.types][.aType].addItems.aValue = aLabel,
119
     * with type specific options merged by pageTsConfig already
120
     *
121
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
122
     *
123
     * @param array $result result array
124
     * @param string $fieldName Current handle field name
125
     * @param array $items Incoming items
126
     * @return array Modified item array
127
     */
128
    protected function addItemsFromPageTsConfig(array $result, $fieldName, array $items)
129
    {
130
        $table = $result['tableName'];
131
        if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'])
132
            && is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'])
133
        ) {
134
            $addItemsArray = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'];
135
            foreach ($addItemsArray as $value => $label) {
136
                // If the value ends with a dot, it is a subelement like "34.icon = mylabel.png", skip it
137
                if (substr($value, -1) === '.') {
138
                    continue;
139
                }
140
                // Check if value "34 = mylabel" also has a "34.icon = myImage.png"
141
                $iconIdentifier = null;
142
                if (isset($addItemsArray[$value . '.'])
143
                    && is_array($addItemsArray[$value . '.'])
144
                    && !empty($addItemsArray[$value . '.']['icon'])
145
                ) {
146
                    $iconIdentifier = $addItemsArray[$value . '.']['icon'];
147
                }
148
                $items[] = [$label, $value, $iconIdentifier];
149
            }
150
        }
151
        return $items;
152
    }
153
154
    /**
155
     * TCA config "special" evaluation. Add them to $items
156
     *
157
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
158
     *
159
     * @param array $result Result array
160
     * @param string $fieldName Current handle field name
161
     * @param array $items Incoming items
162
     * @return array Modified item array
163
     * @throws \UnexpectedValueException
164
     * @deprecated since v11, will be removed in v12
165
     */
166
    protected function addItemsFromSpecial(array $result, $fieldName, array $items)
167
    {
168
        // Guard
169
        if (empty($result['processedTca']['columns'][$fieldName]['config']['special'])
170
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['special'])
171
        ) {
172
            return $items;
173
        }
174
175
        $special = $result['processedTca']['columns'][$fieldName]['config']['special'];
176
177
        trigger_error(
178
            'Using the TCA property \'special=' . $special . '\' is deprecated and will be removed in TYPO3 v12. Use a custom itemsProcFunc instead.',
179
            E_USER_DEPRECATED
180
        );
181
182
        switch (true) {
183
            case $special === 'tables':
184
                $fieldInformation = ['items' => $items];
185
                (new TcaItemsProcessorFunctions())->populateAvailableTables($fieldInformation);
186
                $items = $fieldInformation['items'];
187
                break;
188
            case $special === 'pagetypes':
189
                $fieldInformation = ['items' => $items];
190
                (new TcaItemsProcessorFunctions())->populateAvailablePageTypes($fieldInformation);
191
                $items = $fieldInformation['items'];
192
                break;
193
            case $special === 'exclude':
194
                $fieldInformation = ['items' => $items];
195
                (new TcaItemsProcessorFunctions())->populateExcludeFields($fieldInformation);
196
                $items = $fieldInformation['items'];
197
                break;
198
            case $special === 'explicitValues':
199
                $fieldInformation = ['items' => $items];
200
                (new TcaItemsProcessorFunctions())->populateExplicitAuthValues($fieldInformation);
201
                $items = $fieldInformation['items'];
202
                break;
203
            case $special === 'custom':
204
                $fieldInformation = ['items' => $items];
205
                (new TcaItemsProcessorFunctions())->populateCustomPermissionOptions($fieldInformation);
206
                $items = $fieldInformation['items'];
207
                break;
208
            case $special === 'modListGroup':
209
                $fieldInformation = ['items' => $items];
210
                (new TcaItemsProcessorFunctions())->populateAvailableGroupModules($fieldInformation);
211
                $items = $fieldInformation['items'];
212
                break;
213
            case $special === 'modListUser':
214
                $fieldInformation = ['items' => $items];
215
                (new TcaItemsProcessorFunctions())->populateAvailableUserModules($fieldInformation);
216
                $items = $fieldInformation['items'];
217
                break;
218
            default:
219
                throw new \UnexpectedValueException(
220
                    'Unknown special value ' . $special . ' for field ' . $fieldName . ' of table ' . $result['tableName'],
221
                    1439298496
222
                );
223
        }
224
        return $items;
225
    }
226
227
    /**
228
     * TCA config "fileFolder" evaluation. Add them to $items
229
     *
230
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
231
     *
232
     * @param array $result Result array
233
     * @param string $fieldName Current handle field name
234
     * @param array $items Incoming items
235
     * @return array Modified item array
236
     * @throws \RuntimeException
237
     */
238
    protected function addItemsFromFolder(array $result, $fieldName, array $items)
239
    {
240
        if (empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
241
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder'])
242
        ) {
243
            return $items;
244
        }
245
246
        $fileFolderRaw = $result['processedTca']['columns'][$fieldName]['config']['fileFolder'];
247
        $fileFolder = GeneralUtility::getFileAbsFileName($fileFolderRaw);
248
        if ($fileFolder === '') {
249
            throw new \RuntimeException(
250
                'Invalid folder given for item processing: ' . $fileFolderRaw . ' for table ' . $result['tableName'] . ', field ' . $fieldName,
251
                1479399227
252
            );
253
        }
254
        $fileFolder = rtrim($fileFolder, '/') . '/';
255
256
        if (@is_dir($fileFolder)) {
257
            $fileExtensionList = '';
258
            if (!empty($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
259
                && is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'])
260
            ) {
261
                $fileExtensionList = $result['processedTca']['columns'][$fieldName]['config']['fileFolder_extList'];
262
            }
263
            $recursionLevels = isset($result['processedTca']['columns'][$fieldName]['config']['fileFolder_recursions'])
264
                ? MathUtility::forceIntegerInRange($result['processedTca']['columns'][$fieldName]['config']['fileFolder_recursions'], 0, 99)
265
                : 99;
266
            $fileArray = GeneralUtility::getAllFilesAndFoldersInPath([], $fileFolder, $fileExtensionList, false, $recursionLevels);
267
            $fileArray = GeneralUtility::removePrefixPathFromList($fileArray, $fileFolder);
268
            foreach ($fileArray as $fileReference) {
269
                $fileInformation = pathinfo($fileReference);
270
                $icon = GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], strtolower($fileInformation['extension']))
271
                    ? $fileFolder . $fileReference
272
                    : '';
273
                $items[] = [
274
                    $fileReference,
275
                    $fileReference,
276
                    $icon
277
                ];
278
            }
279
        }
280
281
        return $items;
282
    }
283
284
    /**
285
     * TCA config "foreign_table" evaluation. Add them to $items
286
     *
287
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
288
     *
289
     * @param array $result Result array
290
     * @param string $fieldName Current handle field name
291
     * @param array $items Incoming items
292
     * @return array Modified item array
293
     * @throws \UnexpectedValueException
294
     */
295
    protected function addItemsFromForeignTable(array $result, $fieldName, array $items)
296
    {
297
        $databaseError = null;
298
        $queryResult = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $queryResult is dead and can be removed.
Loading history...
299
        // Guard
300
        if (empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
301
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
302
        ) {
303
            return $items;
304
        }
305
306
        $languageService = $this->getLanguageService();
307
308
        $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
309
310
        if (!isset($GLOBALS['TCA'][$foreignTable]) || !is_array($GLOBALS['TCA'][$foreignTable])) {
311
            throw new \UnexpectedValueException(
312
                'Field ' . $fieldName . ' of table ' . $result['tableName'] . ' reference to foreign table '
313
                . $foreignTable . ', but this table is not defined in TCA',
314
                1439569743
315
            );
316
        }
317
318
        $queryBuilder = $this->buildForeignTableQueryBuilder($result, $fieldName);
319
        try {
320
            $queryResult = $queryBuilder->execute();
321
        } catch (DBALException $e) {
322
            $databaseError = $e->getPrevious()->getMessage();
323
        }
324
325
        // Early return on error with flash message
326
        if (!empty($databaseError)) {
327
            $msg = $databaseError . '. ';
328
            $msg .= $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch');
329
            $msgTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch_title');
330
            /** @var FlashMessage $flashMessage */
331
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, $msgTitle, FlashMessage::ERROR, true);
332
            /** @var FlashMessageService $flashMessageService */
333
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
334
            /** @var FlashMessageQueue $defaultFlashMessageQueue */
335
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
336
            $defaultFlashMessageQueue->enqueue($flashMessage);
337
            return $items;
338
        }
339
340
        $labelPrefix = '';
341
        if (!empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'])) {
342
            $labelPrefix = $result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'];
343
            $labelPrefix = $languageService->sL($labelPrefix);
344
        }
345
346
        $fileRepository = GeneralUtility::makeInstance(FileRepository::class);
347
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
348
349
        while ($foreignRow = $queryResult->fetch()) {
350
            BackendUtility::workspaceOL($foreignTable, $foreignRow);
351
            // Only proceed in case the row was not unset and we don't deal with a delete placeholder
352
            if (is_array($foreignRow)
353
                && !VersionState::cast($foreignRow['t3ver_state'] ?? 0)->equals(VersionState::DELETE_PLACEHOLDER)
354
            ) {
355
                // If the foreign table sets selicon_field, this field can contain an image
356
                // that represents this specific row.
357
                $iconFieldName = '';
358
                $isReferenceField = false;
359
                if (!empty($GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field'])) {
360
                    $iconFieldName = $GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field'];
361
                    if (isset($GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['type'])
362
                        && $GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['type'] === 'inline'
363
                        && $GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['foreign_table'] === 'sys_file_reference'
364
                    ) {
365
                        $isReferenceField = true;
366
                    }
367
                }
368
                $icon = '';
369
                if ($isReferenceField) {
370
                    $references = $fileRepository->findByRelation($foreignTable, $iconFieldName, $foreignRow['uid']);
371
                    if (is_array($references) && !empty($references)) {
372
                        $icon = reset($references);
373
                        $icon = $icon->getPublicUrl();
374
                    }
375
                } else {
376
                    // Else, determine icon based on record type, or a generic fallback
377
                    $icon = $iconFactory->mapRecordTypeToIconIdentifier($foreignTable, $foreignRow);
378
                }
379
                // Add the item
380
                $items[] = [
381
                    $labelPrefix . BackendUtility::getRecordTitle($foreignTable, $foreignRow),
382
                    $foreignRow['uid'],
383
                    $icon
384
                ];
385
            }
386
        }
387
388
        return $items;
389
    }
390
391
    /**
392
     * Remove items using "keepItems" pageTsConfig
393
     *
394
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
395
     *
396
     * @param array $result Result array
397
     * @param string $fieldName Current handle field name
398
     * @param array $items Incoming items
399
     * @return array Modified item array
400
     */
401
    protected function removeItemsByKeepItemsPageTsConfig(array $result, $fieldName, array $items)
402
    {
403
        $table = $result['tableName'];
404
        if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
405
            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
406
        ) {
407
            return $items;
408
        }
409
410
        // If keepItems is set but is an empty list all current items get removed
411
        if ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'] === '') {
412
            return [];
413
        }
414
415
        return ArrayUtility::keepItemsInArray(
416
            $items,
417
            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'],
418
            function ($value) {
419
                return $value[1];
420
            }
421
        );
422
    }
423
424
    /**
425
     * Remove items using "removeItems" pageTsConfig
426
     *
427
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
428
     *
429
     * @param array $result Result array
430
     * @param string $fieldName Current handle field name
431
     * @param array $items Incoming items
432
     * @return array Modified item array
433
     */
434
    protected function removeItemsByRemoveItemsPageTsConfig(array $result, $fieldName, array $items)
435
    {
436
        $table = $result['tableName'];
437
        if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
438
            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
439
            || $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'] === ''
440
        ) {
441
            return $items;
442
        }
443
444
        $removeItems = array_flip(GeneralUtility::trimExplode(
445
            ',',
446
            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'],
447
            true
448
        ));
449
        foreach ($items as $key => $itemValues) {
450
            if (isset($removeItems[$itemValues[1]])) {
451
                unset($items[$key]);
452
            }
453
        }
454
455
        return $items;
456
    }
457
458
    /**
459
     * Remove items user restriction on language field
460
     *
461
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
462
     *
463
     * @param array $result Result array
464
     * @param string $fieldName Current handle field name
465
     * @param array $items Incoming items
466
     * @return array Modified item array
467
     */
468
    protected function removeItemsByUserLanguageFieldRestriction(array $result, $fieldName, array $items)
469
    {
470
        // Guard clause returns if not a language field is handled
471
        if (empty($result['processedTca']['ctrl']['languageField'])
472
            || $result['processedTca']['ctrl']['languageField'] !== $fieldName
473
        ) {
474
            return $items;
475
        }
476
477
        $backendUser = $this->getBackendUser();
478
        foreach ($items as $key => $itemValues) {
479
            if (!$backendUser->checkLanguageAccess($itemValues[1])) {
480
                unset($items[$key]);
481
            }
482
        }
483
484
        return $items;
485
    }
486
487
    /**
488
     * Remove items by user restriction on authMode items
489
     *
490
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
491
     *
492
     * @param array $result Result array
493
     * @param string $fieldName Current handle field name
494
     * @param array $items Incoming items
495
     * @return array Modified item array
496
     */
497
    protected function removeItemsByUserAuthMode(array $result, $fieldName, array $items)
498
    {
499
        // Guard clause returns early if no authMode field is configured
500
        if (!isset($result['processedTca']['columns'][$fieldName]['config']['authMode'])
501
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['authMode'])
502
        ) {
503
            return $items;
504
        }
505
506
        $backendUser = $this->getBackendUser();
507
        $authMode = $result['processedTca']['columns'][$fieldName]['config']['authMode'];
508
        foreach ($items as $key => $itemValues) {
509
            // @todo: checkAuthMode() uses $GLOBAL access for "individual" authMode - get rid of this
510
            if (!$backendUser->checkAuthMode($result['tableName'], $fieldName, $itemValues[1], $authMode)) {
511
                unset($items[$key]);
512
            }
513
        }
514
515
        return $items;
516
    }
517
518
    /**
519
     * Remove items if doktype is handled for non admin users
520
     *
521
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
522
     *
523
     * @param array $result Result array
524
     * @param string $fieldName Current handle field name
525
     * @param array $items Incoming items
526
     * @return array Modified item array
527
     */
528
    protected function removeItemsByDoktypeUserRestriction(array $result, $fieldName, array $items)
529
    {
530
        $table = $result['tableName'];
531
        $backendUser = $this->getBackendUser();
532
        // Guard clause returns if not correct table and field or if user is admin
533
        if ($table !== 'pages' || $fieldName !== 'doktype' || $backendUser->isAdmin()
534
        ) {
535
            return $items;
536
        }
537
538
        $allowedPageTypes = $backendUser->groupData['pagetypes_select'];
539
        foreach ($items as $key => $itemValues) {
540
            if (!GeneralUtility::inList($allowedPageTypes, $itemValues[1])) {
541
                unset($items[$key]);
542
            }
543
        }
544
545
        return $items;
546
    }
547
548
    /**
549
     * Remove items if sys_file_storage is not allowed for non-admin users.
550
     *
551
     * Used by TcaSelectItems data providers
552
     *
553
     * @param array $result Result array
554
     * @param string $fieldName Current handle field name
555
     * @param array $items Incoming items
556
     * @return array Modified item array
557
     */
558
    protected function removeItemsByUserStorageRestriction(array $result, $fieldName, array $items)
559
    {
560
        $referencedTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'] ?? null;
561
        if ($referencedTableName !== 'sys_file_storage') {
562
            return $items;
563
        }
564
565
        $allowedStorageIds = array_map(
566
            function (ResourceStorage $storage) {
567
                return $storage->getUid();
568
            },
569
            $this->getBackendUser()->getFileStorages()
570
        );
571
572
        return array_filter(
573
            $items,
574
            function (array $item) use ($allowedStorageIds) {
575
                $itemValue = $item[1] ?? null;
576
                return empty($itemValue)
577
                    || in_array((int)$itemValue, $allowedStorageIds, true);
578
            }
579
        );
580
    }
581
582
    /**
583
     * Build query to fetch foreign records. Helper method of
584
     * addItemsFromForeignTable(), do not call otherwise.
585
     *
586
     * @param array $result Result array
587
     * @param string $localFieldName Current handle field name
588
     * @return QueryBuilder
589
     */
590
    protected function buildForeignTableQueryBuilder(array $result, string $localFieldName): QueryBuilder
591
    {
592
        $backendUser = $this->getBackendUser();
593
594
        $foreignTableName = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table'];
595
        $foreignTableClauseArray = $this->processForeignTableClause($result, $foreignTableName, $localFieldName);
596
597
        $fieldList = BackendUtility::getCommonSelectFields($foreignTableName, $foreignTableName . '.');
598
        /** @var QueryBuilder $queryBuilder */
599
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
600
            ->getQueryBuilderForTable($foreignTableName);
601
602
        $queryBuilder->getRestrictions()
603
            ->removeAll()
604
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
605
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
606
607
        $queryBuilder
608
            ->select(...GeneralUtility::trimExplode(',', $fieldList, true))
609
            ->from($foreignTableName)
610
            ->where($foreignTableClauseArray['WHERE']);
611
612
        if (!empty($foreignTableClauseArray['GROUPBY'])) {
613
            $queryBuilder->groupBy(...$foreignTableClauseArray['GROUPBY']);
614
        }
615
616
        if (!empty($foreignTableClauseArray['ORDERBY'])) {
617
            foreach ($foreignTableClauseArray['ORDERBY'] as $orderPair) {
618
                [$fieldName, $order] = $orderPair;
619
                $queryBuilder->addOrderBy($fieldName, $order);
620
            }
621
        } elseif (!empty($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby'])) {
622
            $orderByClauses = QueryHelper::parseOrderBy($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby']);
623
            foreach ($orderByClauses as $orderByClause) {
624
                if (!empty($orderByClause[0])) {
625
                    $queryBuilder->addOrderBy($foreignTableName . '.' . $orderByClause[0], $orderByClause[1]);
626
                }
627
            }
628
        }
629
630
        if (!empty($foreignTableClauseArray['LIMIT'])) {
631
            if (!empty($foreignTableClauseArray['LIMIT'][1])) {
632
                $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][1]);
633
                $queryBuilder->setFirstResult($foreignTableClauseArray['LIMIT'][0]);
634
            } elseif (!empty($foreignTableClauseArray['LIMIT'][0])) {
635
                $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][0]);
636
            }
637
        }
638
639
        // rootLevel = -1 means that elements can be on the rootlevel OR on any page (pid!=-1)
640
        // rootLevel = 0 means that elements are not allowed on root level
641
        // rootLevel = 1 means that elements are only on the root level (pid=0)
642
        $rootLevel = 0;
643
        if (isset($GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'])) {
644
            $rootLevel = (int)$GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'];
645
        }
646
647
        if ($rootLevel === -1) {
648
            $queryBuilder->andWhere(
649
                $queryBuilder->expr()->neq(
650
                    $foreignTableName . '.pid',
651
                    $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
652
                )
653
            );
654
        } elseif ($rootLevel === 1) {
655
            $queryBuilder->andWhere(
656
                $queryBuilder->expr()->eq(
657
                    $foreignTableName . '.pid',
658
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
659
                )
660
            );
661
        } else {
662
            $queryBuilder->andWhere($backendUser->getPagePermsClause(Permission::PAGE_SHOW));
663
            if ($foreignTableName !== 'pages') {
664
                $queryBuilder
665
                    ->from('pages')
666
                    ->andWhere(
667
                        $queryBuilder->expr()->eq(
668
                            'pages.uid',
669
                            $queryBuilder->quoteIdentifier($foreignTableName . '.pid')
670
                        )
671
                    );
672
            }
673
        }
674
675
        // @todo what about PID restriction?
676
        if ($this->getBackendUser()->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($foreignTableName)) {
677
            $queryBuilder
678
                ->andWhere(
679
                    $queryBuilder->expr()->neq(
680
                        $foreignTableName . '.t3ver_state',
681
                        $queryBuilder->createNamedParameter(VersionState::MOVE_POINTER, \PDO::PARAM_INT)
682
                    )
683
                );
684
        }
685
686
        return $queryBuilder;
687
    }
688
689
    /**
690
     * Replace markers in a where clause from TCA foreign_table_where
691
     *
692
     * ###REC_FIELD_[field name]###
693
     * ###THIS_UID### - is current element uid (zero if new).
694
     * ###CURRENT_PID### - is the current page id (pid of the record).
695
     * ###SITEROOT###
696
     * ###PAGE_TSCONFIG_ID### - a value you can set from Page TSconfig dynamically.
697
     * ###PAGE_TSCONFIG_IDLIST### - a value you can set from Page TSconfig dynamically.
698
     * ###PAGE_TSCONFIG_STR### - a value you can set from Page TSconfig dynamically.
699
     *
700
     * @param array $result Result array
701
     * @param string $foreignTableName Name of foreign table
702
     * @param string $localFieldName Current handle field name
703
     * @return array Query parts with keys WHERE, ORDERBY, GROUPBY, LIMIT
704
     */
705
    protected function processForeignTableClause(array $result, $foreignTableName, $localFieldName)
706
    {
707
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($foreignTableName);
708
        $localTable = $result['tableName'];
709
        $effectivePid = $result['effectivePid'];
710
711
        $foreignTableClause = '';
712
        if (!empty($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
713
            && is_string($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
714
        ) {
715
            $foreignTableClause = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'];
716
            // Replace possible markers in query
717
            if (strpos($foreignTableClause, '###REC_FIELD_') !== false) {
718
                // " AND table.field='###REC_FIELD_field1###' AND ..." -> array(" AND table.field='", "field1###' AND ...")
719
                $whereClauseParts = explode('###REC_FIELD_', $foreignTableClause);
720
                foreach ($whereClauseParts as $key => $value) {
721
                    if ($key !== 0) {
722
                        // "field1###' AND ..." -> array("field1", "' AND ...")
723
                        $whereClauseSubParts = explode('###', $value, 2);
724
                        // @todo: Throw exception if there is no value? What happens for NEW records?
725
                        $databaseRowKey = empty($result['flexParentDatabaseRow']) ? 'databaseRow' : 'flexParentDatabaseRow';
726
                        $rowFieldValue = $result[$databaseRowKey][$whereClauseSubParts[0]] ?? '';
727
                        if (is_array($rowFieldValue)) {
728
                            // If a select or group field is used here, it may have been processed already and
729
                            // is now an array containing uid + table + title + row.
730
                            // See TcaGroup data provider for details.
731
                            // Pick the first one (always on 0), and use uid only.
732
                            $rowFieldValue = $rowFieldValue[0]['uid'] ?? $rowFieldValue[0];
733
                        }
734
                        if (substr($whereClauseParts[0], -1) === '\'' && $whereClauseSubParts[1][0] === '\'') {
735
                            $whereClauseParts[0] = substr($whereClauseParts[0], 0, -1);
736
                            $whereClauseSubParts[1] = substr($whereClauseSubParts[1], 1);
737
                        }
738
                        $whereClauseParts[$key] = $connection->quote($rowFieldValue) . $whereClauseSubParts[1];
739
                    }
740
                }
741
                $foreignTableClause = implode('', $whereClauseParts);
742
            }
743
            if (strpos($foreignTableClause, '###CURRENT_PID###') !== false) {
744
                // Use pid from parent page clause if in flex form context
745
                if (!empty($result['flexParentDatabaseRow']['pid'])) {
746
                    $effectivePid = $result['flexParentDatabaseRow']['pid'];
747
                } elseif (!$effectivePid && !empty($result['databaseRow']['pid'])) {
748
                    // Use pid from database row if in inline context
749
                    $effectivePid = $result['databaseRow']['pid'];
750
                }
751
            }
752
753
            $siteRootUid = 0;
754
            foreach ($result['rootline'] as $rootlinePage) {
755
                if (!empty($rootlinePage['is_siteroot'])) {
756
                    $siteRootUid = (int)$rootlinePage['uid'];
757
                    break;
758
                }
759
            }
760
761
            $pageTsConfigId = 0;
762
            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'])
763
                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID']
764
            ) {
765
                $pageTsConfigId = (int)$result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'];
766
            }
767
768
            $pageTsConfigIdList = 0;
769
            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'])
770
                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST']
771
            ) {
772
                $pageTsConfigIdList = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'];
773
            }
774
            $pageTsConfigIdListArray = GeneralUtility::trimExplode(',', $pageTsConfigIdList, true);
775
            $pageTsConfigIdList = [];
776
            foreach ($pageTsConfigIdListArray as $pageTsConfigIdListElement) {
777
                if (MathUtility::canBeInterpretedAsInteger($pageTsConfigIdListElement)) {
778
                    $pageTsConfigIdList[] = (int)$pageTsConfigIdListElement;
779
                }
780
            }
781
            $pageTsConfigIdList = implode(',', $pageTsConfigIdList);
782
783
            $pageTsConfigString = '';
784
            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'])
785
                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR']
786
            ) {
787
                $pageTsConfigString = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'];
788
                $pageTsConfigString = $connection->quote($pageTsConfigString);
789
            }
790
791
            $foreignTableClause = str_replace(
792
                [
793
                    '###CURRENT_PID###',
794
                    '###THIS_UID###',
795
                    '###SITEROOT###',
796
                    '###PAGE_TSCONFIG_ID###',
797
                    '###PAGE_TSCONFIG_IDLIST###',
798
                    '\'###PAGE_TSCONFIG_STR###\'',
799
                    '###PAGE_TSCONFIG_STR###'
800
                ],
801
                [
802
                    (int)$effectivePid,
803
                    (int)$result['databaseRow']['uid'],
804
                    $siteRootUid,
805
                    $pageTsConfigId,
806
                    $pageTsConfigIdList,
807
                    $pageTsConfigString,
808
                    $pageTsConfigString
809
                ],
810
                $foreignTableClause
811
            );
812
        }
813
814
        // Split the clause into an array with keys WHERE, GROUPBY, ORDERBY, LIMIT
815
        // Prepend a space to make sure "[[:space:]]+" will find a space there for the first element.
816
        $foreignTableClause = ' ' . $foreignTableClause;
817
        $foreignTableClauseArray = [
818
            'WHERE' => '',
819
            'GROUPBY' => '',
820
            'ORDERBY' => '',
821
            'LIMIT' => '',
822
        ];
823
        // Find LIMIT
824
        $reg = [];
825
        if (preg_match('/^(.*)[[:space:]]+LIMIT[[:space:]]+([[:alnum:][:space:],._]+)$/is', $foreignTableClause, $reg)) {
826
            $foreignTableClauseArray['LIMIT'] = GeneralUtility::intExplode(',', trim($reg[2]), true);
827
            $foreignTableClause = $reg[1];
828
        }
829
        // Find ORDER BY
830
        $reg = [];
831
        if (preg_match('/^(.*)[[:space:]]+ORDER[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) {
832
            $foreignTableClauseArray['ORDERBY'] = QueryHelper::parseOrderBy(trim($reg[2]));
833
            $foreignTableClause = $reg[1];
834
        }
835
        // Find GROUP BY
836
        $reg = [];
837
        if (preg_match('/^(.*)[[:space:]]+GROUP[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) {
838
            $foreignTableClauseArray['GROUPBY'] = QueryHelper::parseGroupBy(trim($reg[2]));
839
            $foreignTableClause = $reg[1];
840
        }
841
        // Rest is assumed to be "WHERE" clause
842
        $foreignTableClauseArray['WHERE'] = QueryHelper::stripLogicalOperatorPrefix($foreignTableClause);
843
844
        return $foreignTableClauseArray;
845
    }
846
847
    /**
848
     * Convert the current database values into an array
849
     *
850
     * @param array $row database row
851
     * @param string $fieldName fieldname to process
852
     * @return array
853
     */
854
    protected function processDatabaseFieldValue(array $row, $fieldName)
855
    {
856
        $currentDatabaseValues = array_key_exists($fieldName, $row)
857
            ? $row[$fieldName]
858
            : '';
859
        if (!is_array($currentDatabaseValues)) {
860
            $currentDatabaseValues = GeneralUtility::trimExplode(',', $currentDatabaseValues, true);
861
        }
862
        return $currentDatabaseValues;
863
    }
864
865
    /**
866
     * Validate and sanitize database row values of the select field with the given name.
867
     * Creates an array out of databaseRow[selectField] values.
868
     *
869
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
870
     *
871
     * @param array $result The current result array.
872
     * @param string $fieldName Name of the current select field.
873
     * @param array $staticValues Array with statically defined items, item value is used as array key.
874
     * @return array
875
     */
876
    protected function processSelectFieldValue(array $result, $fieldName, array $staticValues)
877
    {
878
        $fieldConfig = $result['processedTca']['columns'][$fieldName];
879
880
        $currentDatabaseValueArray = array_key_exists($fieldName, $result['databaseRow']) ? $result['databaseRow'][$fieldName] : [];
881
        $newDatabaseValueArray = [];
882
883
        // Add all values that were defined by static methods and do not come from the relation
884
        // e.g. TCA, TSconfig, itemProcFunc etc.
885
        foreach ($currentDatabaseValueArray as $value) {
886
            if (isset($staticValues[$value])) {
887
                $newDatabaseValueArray[] = $value;
888
            }
889
        }
890
891
        if (isset($fieldConfig['config']['foreign_table']) && !empty($fieldConfig['config']['foreign_table'])) {
892
            /** @var RelationHandler $relationHandler */
893
            $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
894
            $relationHandler->registerNonTableValues = !empty($fieldConfig['config']['allowNonIdValues']);
895
            if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') {
896
                // MM relation
897
                $relationHandler->start(
898
                    implode(',', $currentDatabaseValueArray),
899
                    $fieldConfig['config']['foreign_table'],
900
                    $fieldConfig['config']['MM'],
901
                    $result['databaseRow']['uid'],
902
                    $result['tableName'],
903
                    $fieldConfig['config']
904
                );
905
                $newDatabaseValueArray = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
906
            } else {
907
                // Non MM relation
908
                // If not dealing with MM relations, use default live uid, not versioned uid for record relations
909
                $relationHandler->start(
910
                    implode(',', $currentDatabaseValueArray),
911
                    $fieldConfig['config']['foreign_table'],
912
                    '',
913
                    $this->getLiveUid($result),
914
                    $result['tableName'],
915
                    $fieldConfig['config']
916
                );
917
                $databaseIds = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
918
                // remove all items from the current DB values if not available as relation or static value anymore
919
                $newDatabaseValueArray = array_values(array_intersect($currentDatabaseValueArray, $databaseIds));
920
            }
921
        }
922
923
        if ($fieldConfig['config']['multiple'] ?? false) {
924
            return $newDatabaseValueArray;
925
        }
926
        return array_unique($newDatabaseValueArray);
927
    }
928
929
    /**
930
     * Translate the item labels
931
     *
932
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
933
     *
934
     * @param array $result Result array
935
     * @param array $itemArray Items
936
     * @param string $table
937
     * @param string $fieldName
938
     * @return array
939
     */
940
    public function translateLabels(array $result, array $itemArray, $table, $fieldName)
941
    {
942
        $languageService = $this->getLanguageService();
943
944
        foreach ($itemArray as $key => $item) {
945
            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
946
                && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]])
947
            ) {
948
                $label = $languageService->sL($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$item[1]]);
949
            } else {
950
                $label = $languageService->sL(trim($item[0]));
951
            }
952
            $value = strlen((string)$item[1]) > 0 ? $item[1] : '';
953
            $icon = !empty($item[2]) ? $item[2] : null;
954
            $groupId = $item[3] ?? null;
955
            $helpText = null;
956
            if (!empty($item[4])) {
957
                if (\is_string($item[4])) {
958
                    $helpText = $languageService->sL($item[4]);
959
                } else {
960
                    $helpText = $item[4];
961
                }
962
            }
963
            $itemArray[$key] = [
964
                $label,
965
                $value,
966
                $icon,
967
                $groupId,
968
                $helpText
969
            ];
970
        }
971
972
        return $itemArray;
973
    }
974
975
    /**
976
     * Add alternative icon using "altIcons" TSconfig
977
     *
978
     * @param array $result
979
     * @param array $items
980
     * @param string $table
981
     * @param string $fieldName
982
     *
983
     * @return array
984
     */
985
    public function addIconFromAltIcons(array $result, array $items, string $table, string $fieldName): array
986
    {
987
        foreach ($items as &$item) {
988
            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]])
989
                && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]])
990
            ) {
991
                $item[2] = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item[1]];
992
            }
993
        }
994
995
        return $items;
996
    }
997
998
    /**
999
     * Sanitize incoming item array
1000
     *
1001
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
1002
     *
1003
     * @param mixed $itemArray
1004
     * @param string $tableName
1005
     * @param string $fieldName
1006
     * @throws \UnexpectedValueException
1007
     * @return array
1008
     */
1009
    public function sanitizeItemArray($itemArray, $tableName, $fieldName)
1010
    {
1011
        if (!is_array($itemArray)) {
1012
            $itemArray = [];
1013
        }
1014
        foreach ($itemArray as $item) {
1015
            if (!is_array($item)) {
1016
                throw new \UnexpectedValueException(
1017
                    'An item in field ' . $fieldName . ' of table ' . $tableName . ' is not an array as expected',
1018
                    1439288036
1019
                );
1020
            }
1021
        }
1022
1023
        return $itemArray;
1024
    }
1025
1026
    /**
1027
     * Gets the record uid of the live default record. If already
1028
     * pointing to the live record, the submitted record uid is returned.
1029
     *
1030
     * @param array $result Result array
1031
     * @return int
1032
     * @throws \UnexpectedValueException
1033
     */
1034
    protected function getLiveUid(array $result)
1035
    {
1036
        $table = $result['tableName'];
1037
        $row = $result['databaseRow'];
1038
        $uid = $row['uid'];
1039
        if (BackendUtility::isTableWorkspaceEnabled($table) && (int)$row['t3ver_oid'] > 0) {
1040
            $uid = $row['t3ver_oid'];
1041
        }
1042
        return $uid;
1043
    }
1044
1045
    /**
1046
     * @return LanguageService
1047
     */
1048
    protected function getLanguageService()
1049
    {
1050
        return $GLOBALS['LANG'];
1051
    }
1052
1053
    /**
1054
     * @return BackendUserAuthentication
1055
     */
1056
    protected function getBackendUser()
1057
    {
1058
        return $GLOBALS['BE_USER'];
1059
    }
1060
}
1061