Your IP : 216.73.216.220


Current Path : /var/www/surf/TYPO3/vendor/typo3/cms-backend/Classes/Form/FormDataProvider/
Upload File :
Current File : /var/www/surf/TYPO3/vendor/typo3/cms-backend/Classes/Form/FormDataProvider/AbstractItemProvider.php

<?php

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

namespace TYPO3\CMS\Backend\Form\FormDataProvider;

use Doctrine\DBAL\Exception as DBALException;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\QueryHelper;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Schema\Struct\SelectItem;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Versioning\VersionState;

/**
 * Contains methods used by Data providers that handle elements
 * with single items like select, radio and some more.
 */
abstract class AbstractItemProvider
{
    /**
     * Resolve "itemProcFunc" of elements.
     *
     * @param array $result Main result array
     * @param string $fieldName Field name to handle item list for
     * @param array $items Existing items array
     * @return array New list of item elements
     */
    protected function resolveItemProcessorFunction(array $result, $fieldName, array $items)
    {
        $table = $result['tableName'];
        $config = $result['processedTca']['columns'][$fieldName]['config'];

        $pageTsProcessorParameters = null;
        if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'])) {
            $pageTsProcessorParameters = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'];
        }
        $processorParameters = [
            // Function manipulates $items directly and return nothing
            'items' => &$items,
            'config' => $config,
            'TSconfig' => $pageTsProcessorParameters,
            'table' => $table,
            'row' => $result['databaseRow'],
            'field' => $fieldName,
            // IMPORTANT: Below fields are only available in FormEngine context.
            // They are not used by the DataHandler when processing itemsProcFunc
            // for checking if a submitted value is valid. This means, in case
            // an item is added based on one of these fields, it won't be persisted
            // by the DataHandler. This currently(!) only concerns columns of type "check"
            // and type "radio", see checkValueForCheck() and checkValueForRadio().
            // Therefore, no limitations when using those fields with other types
            // like "select", but this may change in the future.
            'inlineParentUid' => $result['inlineParentUid'],
            'inlineParentTableName' => $result['inlineParentTableName'],
            'inlineParentFieldName' => $result['inlineParentFieldName'],
            'inlineParentConfig' => $result['inlineParentConfig'],
            'inlineTopMostParentUid' => $result['inlineTopMostParentUid'],
            'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'],
            'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'],
        ];
        if (!empty($result['flexParentDatabaseRow'])) {
            $processorParameters['flexParentDatabaseRow'] = $result['flexParentDatabaseRow'];
        }

        try {
            $items = array_map(
                fn(array $item) => SelectItem::fromTcaItemArray($item, $config['type']),
                $items
            );
            GeneralUtility::callUserFunction($config['itemsProcFunc'], $processorParameters, $this);
            $items = array_map(
                fn($item) => $item instanceof SelectItem ? $item : SelectItem::fromTcaItemArray($item, $config['type']),
                $processorParameters['items']
            );
        } catch (\Exception $exception) {
            // The itemsProcFunc method may throw an exception, create a flash message if so
            $languageService = $this->getLanguageService();
            $fieldLabel = $fieldName;
            if (!empty($result['processedTca']['columns'][$fieldName]['label'])) {
                $fieldLabel = $languageService->sL($result['processedTca']['columns'][$fieldName]['label']);
            }
            $message = sprintf(
                $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.items_proc_func_error'),
                $fieldLabel,
                $exception->getMessage()
            );
            $flashMessage = GeneralUtility::makeInstance(
                FlashMessage::class,
                $message,
                '',
                ContextualFeedbackSeverity::ERROR,
                true
            );
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
            $defaultFlashMessageQueue->enqueue($flashMessage);
        }

        return $items;
    }

    /**
     * Page TSconfig addItems:
     *
     * TCEFORMS.aTable.aField[.types][.aType].addItems.aValue = aLabel,
     * with type specific options merged by pageTsConfig already
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result result array
     * @param string $fieldName Current handle field name
     * @param array $items Incoming items
     * @return array Modified item array
     */
    protected function addItemsFromPageTsConfig(array $result, $fieldName, array $items)
    {
        $table = $result['tableName'];
        if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'])
            && is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'])
        ) {
            $addItemsArray = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['addItems.'];
            foreach ($addItemsArray as $value => $label) {
                // If the value ends with a dot, it is a subelement like "34.icon = mylabel.png", skip it
                if (str_ends_with($value, '.')) {
                    continue;
                }
                // Check if value "34 = mylabel" also has a "34.icon = my-icon-identifier"
                // or "34.group = my-group-identifier"
                $iconIdentifier = null;
                $group = null;
                if (is_array($addItemsArray[$value . '.'] ?? null)) {
                    if (!empty($addItemsArray[$value . '.']['icon'])) {
                        $iconIdentifier = $addItemsArray[$value . '.']['icon'];
                    }
                    if (!empty($addItemsArray[$value . '.']['group'])) {
                        $group = $addItemsArray[$value . '.']['group'];
                    }
                }

                $items[] = [
                    'label' => $label,
                    'value' => $value,
                    'icon' => $iconIdentifier,
                    'group' => $group,
                ];
            }
        }
        return $items;
    }

    /**
     * TCA config "fileFolder" evaluation. Add them to $items
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result Result array
     * @param string $fieldName Current handle field name
     * @param array $items Incoming items
     * @return array Modified item array
     * @throws \RuntimeException
     */
    protected function addItemsFromFolder(array $result, $fieldName, array $items)
    {
        if (empty($result['processedTca']['columns'][$fieldName]['config']['fileFolderConfig']['folder'])
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['fileFolderConfig']['folder'])
        ) {
            return $items;
        }

        $tableName = $result['tableName'];
        $fileFolderConfig = $result['processedTca']['columns'][$fieldName]['config']['fileFolderConfig'];
        $fileFolderTSconfig = $result['pageTsConfig']['TCEFORM.'][$tableName . '.'][$fieldName . '.']['config.']['fileFolderConfig.'] ?? [];

        if (is_array($fileFolderTSconfig) && $fileFolderTSconfig !== []) {
            if ($fileFolderTSconfig['folder'] ?? false) {
                $fileFolderConfig['folder'] = $fileFolderTSconfig['folder'];
            }
            if (isset($fileFolderTSconfig['allowedExtensions'])) {
                $fileFolderConfig['allowedExtensions'] = $fileFolderTSconfig['allowedExtensions'];
            }
            if (isset($fileFolderTSconfig['depth'])) {
                $fileFolderConfig['depth'] = (int)$fileFolderTSconfig['depth'];
            }
        }

        $folderRaw = $fileFolderConfig['folder'];
        $folder = GeneralUtility::getFileAbsFileName($folderRaw);
        if ($folder === '') {
            throw new \RuntimeException(
                'Invalid folder given for item processing: ' . $folderRaw . ' for table ' . $tableName . ', field ' . $fieldName,
                1479399227
            );
        }
        $folder = rtrim($folder, '/') . '/';

        if (@is_dir($folder)) {
            $allowedExtensions = '';
            if (!empty($fileFolderConfig['allowedExtensions']) && is_string($fileFolderConfig['allowedExtensions'])) {
                $allowedExtensions = $fileFolderConfig['allowedExtensions'];
            }
            $depth = isset($fileFolderConfig['depth'])
                ? MathUtility::forceIntegerInRange($fileFolderConfig['depth'], 0, 99)
                : 99;
            $fileArray = GeneralUtility::getAllFilesAndFoldersInPath([], $folder, $allowedExtensions, false, $depth);
            $fileArray = GeneralUtility::removePrefixPathFromList($fileArray, $folder);
            foreach ($fileArray as $fileReference) {
                $fileInformation = pathinfo($fileReference);
                $icon = GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], strtolower($fileInformation['extension']))
                    ? $folder . $fileReference
                    : '';
                $items[] = [
                    'label' => $fileReference,
                    'value' => $fileReference,
                    'icon' => $icon,
                ];
            }
        }

        return $items;
    }

    /**
     * TCA config "foreign_table" evaluation. Add them to $items
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result Result array
     * @param string $fieldName Current handled field name
     * @param array $items Incoming items
     * @param bool $includeFullRows @internal Hack for category tree to speed up tree processing, adding full db row as _row to item
     * @return array Modified item array
     * @throws \UnexpectedValueException
     */
    protected function addItemsFromForeignTable(array $result, $fieldName, array $items, bool $includeFullRows = false)
    {
        $databaseError = null;
        $queryResult = null;
        // Guard
        if (empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['foreign_table'])
        ) {
            return $items;
        }

        $languageService = $this->getLanguageService();

        $foreignTable = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];

        if (!isset($GLOBALS['TCA'][$foreignTable]) || !is_array($GLOBALS['TCA'][$foreignTable])) {
            throw new \UnexpectedValueException(
                'Field ' . $fieldName . ' of table ' . $result['tableName'] . ' reference to foreign table '
                . $foreignTable . ', but this table is not defined in TCA',
                1439569743
            );
        }

        $queryBuilder = $this->buildForeignTableQueryBuilder($result, $fieldName, $includeFullRows);
        try {
            $queryResult = $queryBuilder->executeQuery();
        } catch (DBALException $e) {
            $databaseError = $e->getPrevious()->getMessage();
        }

        // Early return on error with flash message
        if (!empty($databaseError)) {
            $msg = $databaseError . '. ';
            $msg .= $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch');
            $msgTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.database_schema_mismatch_title');
            $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $msg, $msgTitle, ContextualFeedbackSeverity::ERROR, true);
            $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
            $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
            $defaultFlashMessageQueue->enqueue($flashMessage);
            return $items;
        }

        $labelPrefix = '';
        if (!empty($result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'])) {
            $labelPrefix = $result['processedTca']['columns'][$fieldName]['config']['foreign_table_prefix'];
            $labelPrefix = $languageService->sL($labelPrefix);
        }

        $fileRepository = GeneralUtility::makeInstance(FileRepository::class);
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);

        $allForeignRows = $queryResult->fetchAllAssociative();
        // Find all possible versioned records of the current IDs, so we do not need to overlay each record
        // This way, workspaceOL() does not need to be called for each record.
        $workspaceId = $this->getBackendUser()->workspace;
        $doOverlaysForRecords = BackendUtility::getPossibleWorkspaceVersionIdsOfLiveRecordIds($foreignTable, array_column($allForeignRows, 'uid'), $workspaceId);

        foreach ($allForeignRows as $foreignRow) {
            // Only do workspace overlays when a versioned record exists.
            if (isset($foreignRow['uid']) && isset($doOverlaysForRecords[(int)$foreignRow['uid']])) {
                BackendUtility::workspaceOL($foreignTable, $foreignRow, $workspaceId);
            }
            // Only proceed in case the row was not unset and we don't deal with a delete placeholder
            if (is_array($foreignRow)
                && !VersionState::cast($foreignRow['t3ver_state'] ?? 0)->equals(VersionState::DELETE_PLACEHOLDER)
            ) {
                // If the foreign table sets selicon_field, this field can contain an image
                // that represents this specific row.
                $iconFieldName = '';
                $isFileReference = false;
                if (!empty($GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field'])) {
                    $iconFieldName = $GLOBALS['TCA'][$foreignTable]['ctrl']['selicon_field'];
                    if (($GLOBALS['TCA'][$foreignTable]['columns'][$iconFieldName]['config']['type'] ?? '') === 'file') {
                        $isFileReference = true;
                    }
                }
                $icon = '';
                if ($isFileReference) {
                    $references = $fileRepository->findByRelation($foreignTable, $iconFieldName, $foreignRow['uid']);
                    if (is_array($references) && !empty($references)) {
                        $icon = reset($references);
                        $icon = $icon->getPublicUrl();
                    }
                } else {
                    // Else, determine icon based on record type, or a generic fallback
                    $icon = $iconFactory->mapRecordTypeToIconIdentifier($foreignTable, $foreignRow);
                }
                $item = [
                    'label' => $labelPrefix . BackendUtility::getRecordTitle($foreignTable, $foreignRow),
                    'value' => $foreignRow['uid'],
                    'icon' => $icon,
                ];
                if ($includeFullRows) {
                    // @todo: This is part of the category tree performance hack
                    $item['_row'] = $foreignRow;
                }
                $items[] = $item;
            }
        }

        return $items;
    }

    /**
     * Remove items using "keepItems" pageTsConfig
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result Result array
     * @param string $fieldName Current handle field name
     * @param array $items Incoming items
     * @return array Modified item array
     */
    protected function removeItemsByKeepItemsPageTsConfig(array $result, $fieldName, array $items)
    {
        $table = $result['tableName'];
        if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'])
        ) {
            return $items;
        }

        // If keepItems is set but is an empty list all current items get removed
        if ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'] === '') {
            return [];
        }

        return ArrayUtility::keepItemsInArray(
            $items,
            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['keepItems'],
            static function ($value) {
                return $value['value'];
            }
        );
    }

    /**
     * Remove items using "removeItems" pageTsConfig
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result Result array
     * @param string $fieldName Current handle field name
     * @param array $items Incoming items
     * @return array Modified item array
     */
    protected function removeItemsByRemoveItemsPageTsConfig(array $result, $fieldName, array $items)
    {
        $table = $result['tableName'];
        if (!isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
            || !is_string($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'])
            || $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'] === ''
        ) {
            return $items;
        }

        $removeItems = array_flip(GeneralUtility::trimExplode(
            ',',
            $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['removeItems'],
            true
        ));
        foreach ($items as $key => $itemValues) {
            if (isset($removeItems[$itemValues['value']])) {
                unset($items[$key]);
            }
        }

        return $items;
    }

    /**
     * Remove items user restriction on language field
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result Result array
     * @param string $fieldName Current handle field name
     * @param array $items Incoming items
     * @return array Modified item array
     */
    protected function removeItemsByUserLanguageFieldRestriction(array $result, $fieldName, array $items)
    {
        // Guard clause returns if not a language field is handled
        if (empty($result['processedTca']['ctrl']['languageField'])
            || $result['processedTca']['ctrl']['languageField'] !== $fieldName
        ) {
            return $items;
        }

        $backendUser = $this->getBackendUser();
        foreach ($items as $key => $itemValues) {
            if (!$backendUser->checkLanguageAccess($itemValues['value'])) {
                unset($items[$key]);
            }
        }

        return $items;
    }

    /**
     * Remove items by user restriction on authMode items
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result Result array
     * @param string $fieldName Current handle field name
     * @param array $items Incoming items
     * @return array Modified item array
     */
    protected function removeItemsByUserAuthMode(array $result, $fieldName, array $items)
    {
        // Guard clause returns early if no authMode field is configured
        if (!isset($result['processedTca']['columns'][$fieldName]['config']['authMode'])
            || !is_string($result['processedTca']['columns'][$fieldName]['config']['authMode'])
        ) {
            return $items;
        }

        $backendUser = $this->getBackendUser();
        foreach ($items as $key => $itemValues) {
            if (!$backendUser->checkAuthMode($result['tableName'], $fieldName, $itemValues['value'])) {
                unset($items[$key]);
            }
        }

        return $items;
    }

    /**
     * Remove items if doktype is handled for non admin users
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result Result array
     * @param string $fieldName Current handle field name
     * @param array $items Incoming items
     * @return array Modified item array
     */
    protected function removeItemsByDoktypeUserRestriction(array $result, $fieldName, array $items)
    {
        $table = $result['tableName'];
        $backendUser = $this->getBackendUser();
        // Guard clause returns if not correct table and field or if user is admin
        if ($table !== 'pages' || $fieldName !== 'doktype' || $backendUser->isAdmin()
        ) {
            return $items;
        }

        $allowedPageTypes = $backendUser->groupData['pagetypes_select'];
        foreach ($items as $key => $itemValues) {
            if (!GeneralUtility::inList($allowedPageTypes, $itemValues['value'])) {
                unset($items[$key]);
            }
        }

        return $items;
    }

    /**
     * Remove items if sys_file_storage is not allowed for non-admin users.
     *
     * Used by TcaSelectItems data providers
     *
     * @param array $result Result array
     * @param string $fieldName Current handle field name
     * @param array $items Incoming items
     * @return array Modified item array
     */
    protected function removeItemsByUserStorageRestriction(array $result, $fieldName, array $items)
    {
        $referencedTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'] ?? null;
        if ($referencedTableName !== 'sys_file_storage') {
            return $items;
        }

        $allowedStorageIds = array_map(
            static function (ResourceStorage $storage) {
                return $storage->getUid();
            },
            $this->getBackendUser()->getFileStorages()
        );

        return array_filter(
            $items,
            static function (array $item) use ($allowedStorageIds) {
                $itemValue = $item['value'] ?? null;
                return empty($itemValue)
                    || in_array((int)$itemValue, $allowedStorageIds, true);
            }
        );
    }

    /**
     * Build query to fetch foreign records. Helper method of
     * addItemsFromForeignTable(), do not call otherwise.
     *
     * @param array $result Result array
     * @param string $localFieldName Current handle field name
     * @param bool $selectAllFields @internal True to select * all fields of row, otherwise an auto-calculated list.
     *                              Select * is an optimization hack to speed up category tree calculation.
     */
    protected function buildForeignTableQueryBuilder(array $result, string $localFieldName, bool $selectAllFields = false): QueryBuilder
    {
        $backendUser = $this->getBackendUser();

        $foreignTableName = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table'];
        $foreignTableClauseArray = $this->processForeignTableClause($result, $foreignTableName, $localFieldName);

        if ($selectAllFields) {
            $fieldList = [$foreignTableName . '.*'];
        } else {
            $fieldList = BackendUtility::getCommonSelectFields($foreignTableName, $foreignTableName . '.');
            $fieldList = GeneralUtility::trimExplode(',', $fieldList, true);
        }

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable($foreignTableName);

        $queryBuilder->getRestrictions()
            ->removeAll()
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
            ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));

        $queryBuilder
            ->select(...$fieldList)
            ->from($foreignTableName)
            ->where($foreignTableClauseArray['WHERE']);

        if (!empty($foreignTableClauseArray['GROUPBY'])) {
            $queryBuilder->groupBy(...$foreignTableClauseArray['GROUPBY']);
        }

        if (!empty($foreignTableClauseArray['ORDERBY'])) {
            foreach ($foreignTableClauseArray['ORDERBY'] as $orderPair) {
                [$fieldName, $order] = $orderPair;
                $queryBuilder->addOrderBy($fieldName, $order);
            }
        } elseif (!empty($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby'])) {
            $orderByClauses = QueryHelper::parseOrderBy($GLOBALS['TCA'][$foreignTableName]['ctrl']['default_sortby']);
            foreach ($orderByClauses as $orderByClause) {
                if (!empty($orderByClause[0])) {
                    $queryBuilder->addOrderBy($foreignTableName . '.' . $orderByClause[0], $orderByClause[1]);
                }
            }
        }

        if (!empty($foreignTableClauseArray['LIMIT'])) {
            if (!empty($foreignTableClauseArray['LIMIT'][1])) {
                $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][1]);
                $queryBuilder->setFirstResult($foreignTableClauseArray['LIMIT'][0]);
            } elseif (!empty($foreignTableClauseArray['LIMIT'][0])) {
                $queryBuilder->setMaxResults($foreignTableClauseArray['LIMIT'][0]);
            }
        }

        // rootLevel = -1 means that elements can be on the rootlevel OR on any page (pid!=-1)
        // rootLevel = 0 means that elements are not allowed on root level
        // rootLevel = 1 means that elements are only on the root level (pid=0)
        $rootLevel = 0;
        if (isset($GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'])) {
            $rootLevel = (int)$GLOBALS['TCA'][$foreignTableName]['ctrl']['rootLevel'];
        }

        if ($rootLevel === -1) {
            $queryBuilder->andWhere(
                $queryBuilder->expr()->neq(
                    $foreignTableName . '.pid',
                    $queryBuilder->createNamedParameter(-1, Connection::PARAM_INT)
                )
            );
        } elseif ($rootLevel === 1) {
            $queryBuilder->andWhere(
                $queryBuilder->expr()->eq(
                    $foreignTableName . '.pid',
                    $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
                )
            );
        } else {
            $queryBuilder->andWhere($backendUser->getPagePermsClause(Permission::PAGE_SHOW));
            if ($foreignTableName !== 'pages') {
                $queryBuilder
                    ->from('pages')
                    ->andWhere(
                        $queryBuilder->expr()->eq(
                            'pages.uid',
                            $queryBuilder->quoteIdentifier($foreignTableName . '.pid')
                        )
                    );
            }
        }

        // @todo what about PID restriction?
        if ($this->getBackendUser()->workspace !== 0 && BackendUtility::isTableWorkspaceEnabled($foreignTableName)) {
            $queryBuilder
                ->andWhere(
                    $queryBuilder->expr()->neq(
                        $foreignTableName . '.t3ver_state',
                        $queryBuilder->createNamedParameter(VersionState::MOVE_POINTER, Connection::PARAM_INT)
                    )
                );
        }

        return $queryBuilder;
    }

    /**
     * Replace markers in a where clause from TCA foreign_table_where
     *
     * ###REC_FIELD_[field name]###
     * ###THIS_UID### - is current element uid (zero if new).
     * ###CURRENT_PID### - is the current page id (pid of the record).
     * ###SITEROOT###
     * ###PAGE_TSCONFIG_ID### - a value you can set from page TSconfig dynamically.
     * ###PAGE_TSCONFIG_IDLIST### - a value you can set from page TSconfig dynamically.
     * ###PAGE_TSCONFIG_STR### - a value you can set from page TSconfig dynamically.
     *
     * @param array $result Result array
     * @param string $foreignTableName Name of foreign table
     * @param string $localFieldName Current handle field name
     * @return array Query parts with keys WHERE, ORDERBY, GROUPBY, LIMIT
     */
    protected function processForeignTableClause(array $result, $foreignTableName, $localFieldName)
    {
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($foreignTableName);
        $localTable = $result['tableName'];
        $effectivePid = $result['effectivePid'];

        $foreignTableClause = '';
        if (!empty($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
            && is_string($result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where'])
        ) {
            $foreignTableClause = QueryHelper::quoteDatabaseIdentifiers($connection, $result['processedTca']['columns'][$localFieldName]['config']['foreign_table_where']);
            // Replace possible markers in query
            if (str_contains($foreignTableClause, '###REC_FIELD_')) {
                // " AND table.field='###REC_FIELD_field1###' AND ..." -> array(" AND table.field='", "field1###' AND ...")
                $whereClauseParts = explode('###REC_FIELD_', $foreignTableClause);
                foreach ($whereClauseParts as $key => $value) {
                    if ($key !== 0) {
                        // "field1###' AND ..." -> array("field1", "' AND ...")
                        $whereClauseSubParts = explode('###', $value, 2);
                        // @todo: Throw exception if there is no value? What happens for NEW records?
                        $databaseRowKey = empty($result['flexParentDatabaseRow']) ? 'databaseRow' : 'flexParentDatabaseRow';
                        $rowFieldValue = $result[$databaseRowKey][$whereClauseSubParts[0]] ?? '';
                        if (is_array($rowFieldValue)) {
                            // If a select or group field is used here, it may have been processed already and
                            // is now an array containing uid + table + title + row.
                            // See TcaGroup data provider for details.
                            // Pick the first one (always on 0), and use uid only.
                            $rowFieldValue = $rowFieldValue[0]['uid'] ?? $rowFieldValue[0] ?? '';
                        }
                        if (substr($whereClauseParts[0], -1) === '\'' && $whereClauseSubParts[1][0] === '\'') {
                            $whereClauseParts[0] = substr($whereClauseParts[0], 0, -1);
                            $whereClauseSubParts[1] = substr($whereClauseSubParts[1], 1);
                        }
                        $whereClauseParts[$key] = $connection->quote($rowFieldValue) . $whereClauseSubParts[1];
                    }
                }
                $foreignTableClause = implode('', $whereClauseParts);
            }
            if (str_contains($foreignTableClause, '###CURRENT_PID###')) {
                // Use pid from parent page clause if in flex form context
                if (!empty($result['flexParentDatabaseRow']['pid'])) {
                    $effectivePid = $result['flexParentDatabaseRow']['pid'];
                } elseif (!$effectivePid && !empty($result['databaseRow']['pid'])) {
                    // Use pid from database row if in inline context
                    $effectivePid = $result['databaseRow']['pid'];
                }
            }

            $siteRootUid = 0;
            foreach ($result['rootline'] as $rootlinePage) {
                if (!empty($rootlinePage['is_siteroot'])) {
                    $siteRootUid = (int)$rootlinePage['uid'];
                    break;
                }
            }

            $pageTsConfigId = 0;
            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'])
                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID']
            ) {
                $pageTsConfigId = (int)$result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_ID'];
            }

            $pageTsConfigIdList = 0;
            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'])
                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST']
            ) {
                $pageTsConfigIdList = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_IDLIST'];
            }
            $pageTsConfigIdListArray = GeneralUtility::trimExplode(',', $pageTsConfigIdList, true);
            $pageTsConfigIdList = [];
            foreach ($pageTsConfigIdListArray as $pageTsConfigIdListElement) {
                if (MathUtility::canBeInterpretedAsInteger($pageTsConfigIdListElement)) {
                    $pageTsConfigIdList[] = (int)$pageTsConfigIdListElement;
                }
            }
            $pageTsConfigIdList = implode(',', $pageTsConfigIdList);

            $pageTsConfigString = '';
            if (isset($result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'])
                && $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR']
            ) {
                $pageTsConfigString = $result['pageTsConfig']['TCEFORM.'][$localTable . '.'][$localFieldName . '.']['PAGE_TSCONFIG_STR'];
                $pageTsConfigString = $connection->quote($pageTsConfigString);
            }

            $foreignTableClause = str_replace(
                [
                    '###CURRENT_PID###',
                    '###THIS_UID###',
                    '###SITEROOT###',
                    '###PAGE_TSCONFIG_ID###',
                    '###PAGE_TSCONFIG_IDLIST###',
                    '\'###PAGE_TSCONFIG_STR###\'',
                    '###PAGE_TSCONFIG_STR###',
                ],
                [
                    (int)$effectivePid,
                    (int)$result['databaseRow']['uid'],
                    $siteRootUid,
                    $pageTsConfigId,
                    $pageTsConfigIdList,
                    $pageTsConfigString,
                    $pageTsConfigString,
                ],
                $foreignTableClause
            );

            $parsedSiteConfiguration = $this->parseSiteConfiguration($result['site'], $foreignTableClause);
            if ($parsedSiteConfiguration !== []) {
                $parsedSiteConfiguration = $this->quoteParsedSiteConfiguration($connection, $parsedSiteConfiguration);
                $foreignTableClause = $this->replaceParsedSiteConfiguration($foreignTableClause, $parsedSiteConfiguration);
            }
        }

        // Split the clause into an array with keys WHERE, GROUPBY, ORDERBY, LIMIT
        // Prepend a space to make sure "[[:space:]]+" will find a space there for the first element.
        $foreignTableClause = ' ' . $foreignTableClause;
        $foreignTableClauseArray = [
            'WHERE' => '',
            'GROUPBY' => '',
            'ORDERBY' => '',
            'LIMIT' => '',
        ];
        // Find LIMIT
        $reg = [];
        if (preg_match('/^(.*)[[:space:]]+LIMIT[[:space:]]+([[:alnum:][:space:],._]+)$/is', $foreignTableClause, $reg)) {
            $foreignTableClauseArray['LIMIT'] = GeneralUtility::intExplode(',', trim($reg[2]), true);
            $foreignTableClause = $reg[1];
        }
        // Find ORDER BY
        $reg = [];
        if (preg_match('/^(.*)[[:space:]]+ORDER[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) {
            $foreignTableClauseArray['ORDERBY'] = QueryHelper::parseOrderBy(trim($reg[2]));
            $foreignTableClause = $reg[1];
        }
        // Find GROUP BY
        $reg = [];
        if (preg_match('/^(.*)[[:space:]]+GROUP[[:space:]]+BY[[:space:]]+([[:alnum:][:space:],._()"]+)$/is', $foreignTableClause, $reg)) {
            $foreignTableClauseArray['GROUPBY'] = QueryHelper::parseGroupBy(trim($reg[2]));
            $foreignTableClause = $reg[1];
        }
        // Rest is assumed to be "WHERE" clause
        $foreignTableClauseArray['WHERE'] = QueryHelper::stripLogicalOperatorPrefix($foreignTableClause);

        return $foreignTableClauseArray;
    }

    /**
     * Parse ###SITE:### placeholders in the input string and return the replacements array for later use in
     * $this->replaceParsedSiteConfiguration().
     *
     * IMPORTANT: If the values are used within raw SQL statements (e.g. foreign_table_where), consider using
     * $this->quoteParsedSiteConfiguration() *before* replacement.
     */
    protected function parseSiteConfiguration(?SiteInterface $site, string $input): array
    {
        // Since we need to access the configuration, early return in case
        // we don't deal with an instance of Site (e.g. null or NullSite).
        if (!$site instanceof Site) {
            return [];
        }

        $siteClausesRegEx = '/###SITE:([^#]+)###/m';
        preg_match_all($siteClausesRegEx, $input, $matches, PREG_SET_ORDER);

        if (empty($matches)) {
            return [];
        }

        $replacements = [];
        $configuration = $site->getConfiguration();
        array_walk($matches, static function ($match) use (&$replacements, &$configuration) {
            $key = $match[1];
            try {
                $value = ArrayUtility::getValueByPath($configuration, $key, '.');
            } catch (MissingArrayPathException $exception) {
                $value = '';
            }

            $replacements[$match[0]] = $value;
        });

        return $replacements;
    }

    protected function quoteParsedSiteConfiguration(Connection $connection, array $parsedSiteConfiguration): array
    {
        foreach ($parsedSiteConfiguration as $key => $value) {
            if (is_int($value)) {
                // int values are safe, nothing to do here
                continue;
            }
            if (is_string($value)) {
                $parsedSiteConfiguration[$key] = $connection->quote($value);
                continue;
            }
            if (is_array($value)) {
                $parsedSiteConfiguration[$key] = implode(',', $this->quoteParsedSiteConfiguration($connection, $value));
                continue;
            }
            if (is_bool($value)) {
                $parsedSiteConfiguration[$key] = (int)$value;
                continue;
            }
            throw new \InvalidArgumentException(
                sprintf('Cannot quote site configuration setting "%s" of type "%s", only "int", "bool", "string" and "array" are supported', $key, gettype($value)),
                1630324435
            );
        }

        return $parsedSiteConfiguration;
    }

    protected function replaceParsedSiteConfiguration(string $input, array $parsedSiteConfiguration): string
    {
        return str_replace(
            array_keys($parsedSiteConfiguration),
            array_values($parsedSiteConfiguration),
            $input
        );
    }

    /**
     * A field's [treeConfig][startingPoints] can be set via site config, parse possibly set values
     */
    protected function parseStartingPointsFromSiteConfiguration(array $result, array $fieldConfig): array
    {
        if (!isset($fieldConfig['config']['treeConfig']['startingPoints'])) {
            return $fieldConfig;
        }

        $parsedSiteConfiguration = $this->parseSiteConfiguration($result['site'], $fieldConfig['config']['treeConfig']['startingPoints']);
        if ($parsedSiteConfiguration !== []) {
            // $this->quoteParsedSiteConfiguration() is omitted on purpose, all values are cast to integers
            $parsedSiteConfiguration = array_unique(array_map(static function ($value): string {
                if (is_array($value)) {
                    return implode(',', array_map('intval', $value));
                }

                return implode(',', GeneralUtility::intExplode(',', (string)$value, true));
            }, $parsedSiteConfiguration));
            $resolvedStartingPoints = $this->replaceParsedSiteConfiguration($fieldConfig['config']['treeConfig']['startingPoints'], $parsedSiteConfiguration);
            // Add the resolved starting points while removing empty values
            $fieldConfig['config']['treeConfig']['startingPoints'] = implode(
                ',',
                GeneralUtility::trimExplode(',', $resolvedStartingPoints, true)
            );
        }

        return $fieldConfig;
    }

    /**
     * Convert the current database values into an array
     *
     * @param array $row database row
     * @param string $fieldName fieldname to process
     * @return array
     */
    protected function processDatabaseFieldValue(array $row, $fieldName)
    {
        $currentDatabaseValues = array_key_exists($fieldName, $row)
            ? $row[$fieldName]
            : '';
        if (!is_array($currentDatabaseValues)) {
            $currentDatabaseValues = GeneralUtility::trimExplode(',', $currentDatabaseValues, true);
        }
        return $currentDatabaseValues;
    }

    /**
     * Validate and sanitize database row values of the select field with the given name.
     * Creates an array out of databaseRow[selectField] values.
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result The current result array.
     * @param string $fieldName Name of the current select field.
     * @param array $staticValues Array with statically defined items, item value is used as array key.
     * @return array
     */
    protected function processSelectFieldValue(array $result, $fieldName, array $staticValues)
    {
        $fieldConfig = $result['processedTca']['columns'][$fieldName];

        $currentDatabaseValueArray = array_key_exists($fieldName, $result['databaseRow']) ? $result['databaseRow'][$fieldName] : [];
        $newDatabaseValueArray = [];

        // Add all values that were defined by static methods and do not come from the relation
        // e.g. TCA, TSconfig, itemProcFunc etc.
        foreach ($currentDatabaseValueArray as $value) {
            if (isset($staticValues[$value])) {
                $newDatabaseValueArray[] = $value;
            }
        }

        if (isset($fieldConfig['config']['foreign_table']) && !empty($fieldConfig['config']['foreign_table'])) {
            $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
            $relationHandler->registerNonTableValues = !empty($fieldConfig['config']['allowNonIdValues']);
            if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') {
                // MM relation
                $relationHandler->start(
                    implode(',', $currentDatabaseValueArray),
                    $fieldConfig['config']['foreign_table'],
                    $fieldConfig['config']['MM'],
                    $result['databaseRow']['uid'],
                    $result['tableName'],
                    $fieldConfig['config']
                );
                $relationHandler->processDeletePlaceholder();
                $newDatabaseValueArray = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
            } else {
                // Non MM relation
                // If not dealing with MM relations, use default live uid, not versioned uid for record relations
                $relationHandler->start(
                    implode(',', $currentDatabaseValueArray),
                    $fieldConfig['config']['foreign_table'],
                    '',
                    $this->getLiveUid($result),
                    $result['tableName'],
                    $fieldConfig['config']
                );
                $relationHandler->processDeletePlaceholder();
                $databaseIds = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
                // remove all items from the current DB values if not available as relation or static value anymore
                $newDatabaseValueArray = array_values(array_intersect($currentDatabaseValueArray, $databaseIds));
            }
        }

        if ($fieldConfig['config']['multiple'] ?? false) {
            return $newDatabaseValueArray;
        }
        return array_unique($newDatabaseValueArray);
    }

    /**
     * Translate the item labels
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param array $result Result array
     * @param array $itemArray Items
     * @param string $table
     * @param string $fieldName
     * @return array
     */
    public function translateLabels(array $result, array $itemArray, $table, $fieldName)
    {
        $languageService = $this->getLanguageService();

        foreach ($itemArray as $key => $item) {
            $labelIndex = $item['value'] ?? '';

            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$labelIndex])
                && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$labelIndex])
            ) {
                $label = $languageService->sL($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'][$labelIndex]);
            } else {
                $label = $languageService->sL(trim($item['label'] ?? ''));
            }
            $value = strlen((string)($item['value'] ?? '')) > 0 ? $item['value'] : '';
            $icon = !empty($item['icon']) ? $item['icon'] : null;
            $groupId = $item['group'] ?? null;
            $helpText = null;
            if (!empty($item['description'])) {
                if (is_string($item['description'])) {
                    $helpText = $languageService->sL($item['description']);
                } else {
                    $helpText = $item['description'];
                }
            }
            $itemArray[$key] = [
                'label' => $label,
                'value' => $value,
                'icon' => $icon,
                'group' => $groupId,
                'description' => $helpText,
            ];
        }

        return $itemArray;
    }

    /**
     * Add alternative icon using "altIcons" TSconfig
     */
    public function addIconFromAltIcons(array $result, array $items, string $table, string $fieldName): array
    {
        foreach ($items as &$item) {
            if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item['value']])
                && !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item['value']])
            ) {
                $item['icon'] = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altIcons.'][$item['value']];
            }
        }

        return $items;
    }

    /**
     * Sanitize incoming item array
     *
     * Used by TcaSelectItems and TcaSelectTreeItems data providers
     *
     * @param mixed $itemArray
     * @param string $tableName
     * @param string $fieldName
     * @throws \UnexpectedValueException
     * @return array
     */
    public function sanitizeItemArray($itemArray, $tableName, $fieldName)
    {
        if (!is_array($itemArray)) {
            $itemArray = [];
        }
        foreach ($itemArray as $item) {
            if (!is_array($item)) {
                throw new \UnexpectedValueException(
                    'An item in field ' . $fieldName . ' of table ' . $tableName . ' is not an array as expected',
                    1439288036
                );
            }
        }

        return $itemArray;
    }

    /**
     * Gets the record uid of the live default record. If already
     * pointing to the live record, the submitted record uid is returned.
     *
     * @param array $result Result array
     * @return int|string If the record is new, uid will be a string beginning with "NEW". Otherwise an int.
     * @throws \UnexpectedValueException
     */
    protected function getLiveUid(array $result)
    {
        $table = $result['tableName'];
        $row = $result['databaseRow'];
        $uid = $row['uid'] ?? 0;
        if (BackendUtility::isTableWorkspaceEnabled($table) && (int)($row['t3ver_oid'] ?? 0) > 0) {
            $uid = $row['t3ver_oid'];
        }
        return $uid;
    }

    protected function getLanguageService(): LanguageService
    {
        return $GLOBALS['LANG'];
    }

    protected function getBackendUser(): BackendUserAuthentication
    {
        return $GLOBALS['BE_USER'];
    }
}