Files

1308 lines
59 KiB
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\Driver\Exception as DBALException;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Configuration\Processor\Placeholder\EnvPlaceholderProcessor;
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\DataHandling\ItemProcessingService;
use TYPO3\CMS\Core\DataHandling\ItemsProcessorContext;
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\Package\PackageManager;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Schema\Capability\RootLevelCapability;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\Struct\SelectItemCollection;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\SystemResource\Exception\InvalidSystemResourceIdentifierException;
use TYPO3\CMS\Core\SystemResource\Identifier\PackageResourceIdentifier;
use TYPO3\CMS\Core\SystemResource\Identifier\SystemResourceIdentifierFactory;
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
{
private IconFactory $iconFactory;
private FileRepository $fileRepository;
private FlashMessageService $flashMessageService;
private ConnectionPool $connectionPool;
private ItemProcessingService $itemProcessingService;
private EnvPlaceholderProcessor $envPlaceholderProcessor;
public function injectIconFactory(IconFactory $iconFactory): void
{
$this->iconFactory = $iconFactory;
}
public function injectFileRepository(FileRepository $fileRepository): void
{
$this->fileRepository = $fileRepository;
}
public function injectFlashMessageService(FlashMessageService $flashMessageService): void
{
$this->flashMessageService = $flashMessageService;
}
public function injectConnectionPool(ConnectionPool $connectionPool): void
{
$this->connectionPool = $connectionPool;
}
public function injectItemProcessingService(ItemProcessingService $itemProcessingService): void
{
$this->itemProcessingService = $itemProcessingService;
}
public function injectEnvPlaceholderProcessor(EnvPlaceholderProcessor $envPlaceholderProcessor): void
{
$this->envPlaceholderProcessor = $envPlaceholderProcessor;
}
/**
* Resolve "itemsProcFunc" 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 resolveItemsProcessorFunction(array $result, $fieldName, array $items)
{
$table = $result['tableName'];
$config = $result['processedTca']['columns'][$fieldName]['config'];
// Pass along only itemsProcFunc or itemsProcessors TSconfig
$pageTsProcessorParameters = [];
if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'])) {
$pageTsProcessorParameters['itemsProcFunc.'] = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcFunc.'];
}
if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcessors.'])) {
$pageTsProcessorParameters['itemsProcessors.'] = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['itemsProcessors.'];
}
$additionalParameters = [
// 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'])) {
$additionalParameters['flexParentDatabaseRow'] = $result['flexParentDatabaseRow'];
}
try {
$itemsCollection = SelectItemCollection::createFromArray($items, $config['type']);
$context = new ItemsProcessorContext(
table: $result['tableName'],
field: $fieldName,
row: $result['databaseRow'],
fieldConfiguration: $config,
processorParameters: [],
realPid: $result['effectivePid'],
site: $result['site'] ?? $this->itemProcessingService->resolveSite($result['effectivePid']),
fieldTSconfig: $pageTsProcessorParameters,
additionalParameters: $additionalParameters
);
$items = $this->itemProcessingService->processItems($itemsCollection, $context)->toArray();
} 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 = new FlashMessage(
$message,
'',
ContextualFeedbackSeverity::ERROR,
true
);
$defaultFlashMessageQueue = $this->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'];
}
}
$folderResource = rtrim($fileFolderConfig['folder'], '/') . '/';
// @todo this needs to be replaced with usage of the yet to be created
// System Resource API that can handle folders
// This will then remove the need to use internal SystemResourceIdentifierFactory here
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
$identifierFactory = new SystemResourceIdentifierFactory($packageManager);
try {
$resourceIdentifier = $identifierFactory->create($folderResource);
if (!$resourceIdentifier instanceof PackageResourceIdentifier) {
throw new InvalidSystemResourceIdentifierException('Only package identifiers are allowed', 1764503770);
}
$folder = $resourceIdentifier->getPackage()->getPackagePath() . $resourceIdentifier->getRelativePath();
} catch (InvalidSystemResourceIdentifierException $e) {
throw new \RuntimeException(
'Invalid folder given for item processing: ' . $folderResource . ' for table ' . $tableName . ', field ' . $fieldName,
1479399227,
$e,
);
}
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']))
? $folderResource . $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
* @return array Modified item array
* @throws \UnexpectedValueException
*/
protected function addItemsFromForeignTable(array $result, string $fieldName, array $items = []): array
{
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 (!$result['tcaSchemata']->has($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);
try {
$queryResult = $queryBuilder->executeQuery();
} catch (DBALException $e) {
// Early return on error with flash message
$msg = $e->getMessage() . '. ' . $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 = new FlashMessage($msg, $msgTitle, ContextualFeedbackSeverity::ERROR, true);
$defaultFlashMessageQueue = $this->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);
}
$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);
$itemGroupField = $result['processedTca']['columns'][$fieldName]['config']['foreign_table_item_group'] ?? '';
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::tryFrom($foreignRow['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER
) {
// If the foreign table sets selicon_field, this field can contain an image
// that represents this specific row.
$iconFieldName = '';
$isFileReference = false;
$foreignTableSchema = $result['tcaSchemata']->get($foreignTable);
if (!empty($foreignTableSchema->getRawConfiguration()['selicon_field'] ?? null)) {
$iconFieldName = $foreignTableSchema->getRawConfiguration()['selicon_field'];
if ($foreignTableSchema->getField($iconFieldName)->getType() === 'file') {
$isFileReference = true;
}
}
$icon = '';
if ($isFileReference) {
$references = $this->fileRepository->findByRelation($foreignTable, $iconFieldName, $foreignRow['uid']);
if (!empty($references)) {
$icon = reset($references);
$icon = $icon->getPublicUrl();
}
} else {
// Else, determine icon based on record type, or a generic fallback
$icon = $this->iconFactory->mapRecordTypeToIconIdentifier($foreignTable, $foreignRow, $result['tcaSchemata']->get($foreignTable));
}
$item = [
'label' => $labelPrefix . BackendUtility::getRecordTitle($foreignTable, $foreignRow),
'value' => $foreignRow['uid'],
'icon' => $icon,
'group' => $foreignRow[$itemGroupField] ?? null,
// This line is part of the category tree performance hack, which should be used everywhere
'_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): int {
return $storage->getUid();
},
$this->getBackendUser()->getFileStorages()
);
return array_filter(
$items,
static function (array $item) use ($allowedStorageIds): bool {
$itemValue = $item['value'] ?? null;
return empty($itemValue)
|| in_array((int)$itemValue, $allowedStorageIds, true);
}
);
}
/**
* Build wrapped QueryBuilder to fetch full foreign records. Helper method of
* {@see self::addItemsFromForeignTable()}, do not call otherwise.
*
* @param array $result Result array
* @param string $localFieldName Current handle field name
*/
protected function buildForeignTableQueryBuilder(array $result, string $localFieldName): QueryBuilder
{
$backendUser = $this->getBackendUser();
$foreignTableName = $result['processedTca']['columns'][$localFieldName]['config']['foreign_table'];
$foreignTableClauseArray = $this->processForeignTableClause($result, $foreignTableName, $localFieldName);
$connection = $this->connectionPool->getConnectionForTable($foreignTableName);
$wrapQueryBuilder = $connection->createQueryBuilder();
$queryBuilder = $connection->createQueryBuilder();
// Full foreign table row is wanted for the result, which requires to have `GROUP BY` columns listed
// within the `SELECT <fields>` list for some database systems and vice versa. Second requirement is,
// that all fields used for `GROUP BY` and listed as select fields needs to be aggregated with a proper
// function, for example (MIN(), MAX(), ANY_VALUES(), ...).
//
// MariaDB is even stricter than MySQL with default and recommend `sql_mode = 'ONLY_FULL_GROUP_BY',
// which is also a long time questioned fact why MariaDB differs here from MySQL and also PostgresSQL
// without an explicit mode setting.
//
// To sum up all requirements for all database systems and expectable modes, we ...
//
// * can't simply select all foreign table fields, for example with `$foreignTableName . '.*'` as select()
// for the QueryBuilder below.
// * need to ensure that we have grouped fields aggregated.
// * need to use a wrapped SQL query (QueryBuilder) to retrieve full rows of foreign table because we cannot
// use full-table columns information to replace a `*` wildcard here.
//
// The first step respecting all requirements is, to determine commonly used select fields for the table
// and using `ANY_VALUES()` aggregation for the `uid` field.
$hasGroupBy = is_array($foreignTableClauseArray['GROUPBY']) && $foreignTableClauseArray['GROUPBY'] !== [];
$selectFieldList = [];
$schema = $result['tcaSchemata']->get($foreignTableName);
$commonFieldList = $this->getCommonSelectFields($foreignTableName, $schema);
foreach ($commonFieldList as $fieldName) {
if ($hasGroupBy && in_array($fieldName, $foreignTableClauseArray['GROUPBY'], true)) {
$selectFieldList[] = sprintf('ANY_VALUE(%s)', $queryBuilder->quoteIdentifier($fieldName));
continue;
}
$selectFieldList[] = $queryBuilder->quoteIdentifier($fieldName);
}
$wrapQueryBuilder->getRestrictions()->removeAll();
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
$queryBuilder
->selectLiteral(...$selectFieldList)
->from($foreignTableName)
->where($foreignTableClauseArray['WHERE']);
if ($hasGroupBy) {
$queryBuilder->groupBy(...$foreignTableClauseArray['GROUPBY']);
}
// Sorting is applied to the inner query to ensure proper sorting and in combination with LIMIT to retrieve
// the expected/correct dataset and not cutting away data. Sorting is applied a second time below on the
// outer joined table (wrapping QueryBuilder) to keep the sorting between inner and outer query for vendors
// applying deterministic sorting criterias and returning other results than other vendors to ensure the same
// behavior across all supported database vendors.
if (!empty($foreignTableClauseArray['ORDERBY'])) {
foreach ($foreignTableClauseArray['ORDERBY'] as $orderPair) {
[$fieldName, $order] = $orderPair;
$queryBuilder->addOrderBy($fieldName, $order);
}
} elseif ($schema->hasCapability(TcaSchemaCapability::DefaultSorting)) {
$orderByClauses = QueryHelper::parseOrderBy($schema->getCapability(TcaSchemaCapability::DefaultSorting)->getValue());
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)
/** @var RootLevelCapability $rootLevelCapability */
$rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel);
if ($rootLevelCapability->getRootLevelType() === -1) {
$queryBuilder->andWhere(
$queryBuilder->expr()->neq(
$foreignTableName . '.pid',
$wrapQueryBuilder->createNamedParameter(-1, Connection::PARAM_INT)
)
);
} elseif ($rootLevelCapability->getRootLevelType() === 1) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq(
$foreignTableName . '.pid',
$wrapQueryBuilder->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 && $schema->hasCapability(TcaSchemaCapability::Workspace)) {
$queryBuilder
->andWhere(
$queryBuilder->expr()->neq(
$foreignTableName . '.t3ver_state',
$wrapQueryBuilder->createNamedParameter(VersionState::MOVE_POINTER->value, Connection::PARAM_INT)
)
);
}
// Second step to respect all database requirements regarding `GROUP BY` and still returning full foreign table
// records is using the QueryBuilder (query) as a sub-query, join the table and retrieve the full records using
// column wildcard. That ensures that really the full records are retrieved including not TCA managed columns.
// @todo Look in this again and consider using a non-recursive CTE here instead of a sub-query table when
// upstream doctrine/dbal CTE support is completely available and working.
$wrapQueryBuilder->select('joined_table.*');
$wrapQueryBuilder->getConcreteQueryBuilder()->from(
'(' . $queryBuilder->getSQL() . ')',
$wrapQueryBuilder->quoteIdentifier('inner_table_alias')
);
$wrapQueryBuilder->innerJoin(
'inner_table_alias',
$foreignTableName,
'joined_table',
$wrapQueryBuilder->expr()->and(
$wrapQueryBuilder->expr()->eq('joined_table.uid', $wrapQueryBuilder->quoteIdentifier('inner_table_alias.uid'))
)
);
// Adding orderings to the re-joined table result is required to ensure that a non-joined table PK sorting
// leads to the same result. For example MariaDB/MySQL applies a deterministic joined table PK sorting and
// other platforms like PostgresSQL does this not. That leads to a re-sorting on the outer result based on
// the automatic sorting override for MySQL/MariaDB. INNER sorting above is still required to have proper
// result in case limiting the resultset (LIMIT) is applied to the foreign table and would otherwise lead
// to retrieving unexpected datasets.
$orderByClauses = [];
if (!empty($foreignTableClauseArray['ORDERBY'])) {
$orderByClauses = $foreignTableClauseArray['ORDERBY'];
} elseif ($schema->hasCapability(TcaSchemaCapability::DefaultSorting)) {
$orderByClauses = QueryHelper::parseOrderBy($schema->getCapability(TcaSchemaCapability::DefaultSorting)->getValue());
}
if ($orderByClauses !== []) {
foreach ($orderByClauses as $orderPair) {
[$fieldName, $order] = $orderPair;
// Remove table name
if (str_contains($fieldName, '.')) {
$fieldName = substr($fieldName, strpos($fieldName, '.') + 1);
}
$fieldName = 'joined_table.' . $fieldName;
$wrapQueryBuilder->addOrderBy($fieldName, $order);
}
}
return $wrapQueryBuilder;
}
/**
* 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 = $this->connectionPool->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 (str_ends_with($whereClauseParts[0], '\'') && $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###',
],
[
(string)(int)$effectivePid,
(string)(int)$result['databaseRow']['uid'],
(string)$siteRootUid,
(string)$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 (array $match) use (&$replacements, &$configuration): void {
$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 (array|string|int $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 ($currentDatabaseValues === null) {
return [];
}
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] : [];
$isSiteAction = $result['tableName'] === 'site';
if ($isSiteAction && count($currentDatabaseValueArray) === 1 && $this->envPlaceholderProcessor->canProcess($currentDatabaseValueArray[0])) {
return $currentDatabaseValueArray;
}
$newDatabaseValueArray = [];
// Add all values that were defined by static methods and do not come from the relation
// e.g. TCA, TSconfig, itemsProcFunc 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 ($labelIndex === ''
&& isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels'])
&& !empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels'])
) {
$label = $languageService->sL($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels']);
} elseif (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'];
}
}
// @todo This removes `_row` full item row and does not have it in processedTCA later, at lest for
// TcaSelectItems. Consider to keep that information here if available or if dropping is good.
$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 ($result['tcaSchemata']->has($table) && $result['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Workspace) && (int)($row['t3ver_oid'] ?? 0) > 0) {
$uid = $row['t3ver_oid'];
}
return $uid;
}
/**
* @internal private on purpose
*/
private function getCommonSelectFields(string $table, TcaSchema $schema): array
{
$fields = ['uid', 'pid'];
if ($schema->hasCapability(TcaSchemaCapability::Label)) {
$fields = array_merge($fields, $schema->getCapability(TcaSchemaCapability::Label)->getAllLabelFieldNames());
}
if ($schema->isWorkspaceAware()) {
$fields[] = 't3ver_state';
$fields[] = 't3ver_wsid';
}
if ($schema->getRawConfiguration()['selicon_field'] ?? '') {
$fields[] = $schema->getRawConfiguration()['selicon_field'];
}
if ($schema->getRawConfiguration()['typeicon_column'] ?? '') {
$fields[] = $schema->getRawConfiguration()['typeicon_column'];
}
$capabilities = [
TcaSchemaCapability::SoftDelete,
TcaSchemaCapability::RestrictionDisabledField,
TcaSchemaCapability::RestrictionStartTime,
TcaSchemaCapability::RestrictionEndTime,
TcaSchemaCapability::RestrictionUserGroup,
];
foreach ($capabilities as $capability) {
if ($schema->hasCapability($capability)) {
$fields[] = $schema->getCapability($capability)->getFieldName();
}
}
$fields = array_unique($fields);
return array_map(static fn(string $value): string => $table . '.' . $value, $fields);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}