1308 lines
59 KiB
PHP
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'];
|
|
}
|
|
}
|