TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,90 @@
<?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 TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Extended by other provider that fetch records from database
*/
abstract class AbstractDatabaseRecordProvider
{
private ConnectionPool $connectionPool;
public function injectConnectionPool(ConnectionPool $connectionPool): void
{
$this->connectionPool = $connectionPool;
}
/**
* Fetch a record from database. Deleted records will NOT be fetched.
* Method is similar to BackendUtility::getRecord, but is more picky
* about input and result.
*
* @param string $tableName The table name to fetch record from
* @param int $uid Uid of record to fetch
* @return array Fetched record row
* @throws DatabaseRecordException|\InvalidArgumentException|\UnexpectedValueException|\RuntimeException
*/
protected function getRecordFromDatabase($tableName, $uid)
{
// @todo Remove int cast after making method parameters native typed.
$uid = (int)$uid;
if ($uid <= 0) {
throw new \InvalidArgumentException(
'$uid must be positive integer, ' . $uid . ' given',
1437656456
);
}
$row = $this->getDatabaseRow($tableName, $uid);
if (empty($row)) {
// Indicates a runtime error (eg. record was killed by other editor meanwhile) can be caught elsewhere
// and transformed to a message to the user or something
throw new DatabaseRecordException(
'Record with uid ' . $uid . ' from table ' . $tableName . ' not found',
1437656081,
null,
$tableName,
$uid
);
}
return BackendUtility::convertDatabaseRowValuesToPhp($tableName, $row);
}
/**
* Retrieve the requested row from the database
*/
protected function getDatabaseRow(string $tableName, int $uid): array
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$row = $queryBuilder->select('*')
->from($tableName)
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)))
->executeQuery()
->fetchAssociative();
return $row ?: [];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
/**
* Fetch page in default language from database if it's a translated pages record
*/
class DatabaseDefaultLanguagePageRow extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
/**
* Add default language page row of existing row to result
* defaultLanguagePageRow will stay NULL in result if a record is added or edited below root node
*
* @return array
*/
public function addData(array $result)
{
// $defaultLanguagePageRow end up NULL if a record added or edited on root node
$tableName = $result['tableName'];
if ($tableName === 'pages'
&& ($tableSchema = $result['tcaSchemata']->get($tableName))
&& $tableSchema->isLanguageAware()
&& ($result['databaseRow'][$tableSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()] ?? 0) > 0) {
$result['defaultLanguagePageRow'] = $this->getRecordFromDatabase('pages', $result['databaseRow'][$tableSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()]);
}
return $result;
}
}
@@ -0,0 +1,69 @@
<?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 TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordWorkspaceDeletePlaceholderException;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Fetch existing database row on edit
*/
class DatabaseEditRow extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
/**
* Fetch existing record from database
*
* @return array
* @throws \UnexpectedValueException
* @throws DatabaseRecordWorkspaceDeletePlaceholderException
* @throws DatabaseRecordException
*/
public function addData(array $result)
{
if ($result['command'] !== 'edit' || !empty($result['databaseRow'])) {
return $result;
}
$databaseRow = $this->getRecordFromDatabase($result['tableName'], $result['vanillaUid']);
if (!array_key_exists('pid', $databaseRow)) {
throw new \UnexpectedValueException(
'Parent record does not have a pid field',
1437663061
);
}
if ($result['tcaSchemata']->has($result['tableName'])
&& $result['tcaSchemata']->get($result['tableName'])->hasCapability(TcaSchemaCapability::Workspace)
&& VersionState::tryFrom($databaseRow['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER
) {
// Workspace delete placeholder records (t3ver_state = 2) should never be edited. This is a fallback
// to suppress editing in case something still links to FormEngine edit of such a record.
throw new DatabaseRecordWorkspaceDeletePlaceholderException(
'Record with uid "' . $databaseRow['uid'] . '" from table "' . $result['tableName'] . '" is'
. ' a workspace delete placeholder record which can not be edited.',
1608658396,
$result['tableName'],
(int)$databaseRow['uid']
);
}
$result['databaseRow'] = $databaseRow;
return $result;
}
}
@@ -0,0 +1,55 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Set effective pid we're working on
*/
class DatabaseEffectivePid implements FormDataProviderInterface
{
/**
* Effective pid is used to determine entry point for page ts and is also
* the pid where new records are stored later.
*
* @return array
*/
public function addData(array $result)
{
$effectivePid = 0;
if ($result['command'] === 'edit' && $result['tableName'] === 'pages') {
// We always need to detect the "live record of the default language"
// Translated pages should always point to UID of the default language as "pid" anyway.
// Good to know: l10n_parent in a translated versioned record (double-overlay) points
// to the "live record of the default language"
if (isset($result['databaseRow']['l10n_parent']) && $result['databaseRow']['l10n_parent'] > 0) {
$effectivePid = $result['databaseRow']['l10n_parent'];
} elseif (isset($result['databaseRow']['t3ver_oid']) && $result['databaseRow']['t3ver_oid'] > 0) {
$effectivePid = $result['databaseRow']['t3ver_oid'];
} else {
$effectivePid = $result['databaseRow']['uid'];
}
} elseif ($result['command'] === 'edit') {
$effectivePid = $result['databaseRow']['pid'];
} elseif ($result['command'] === 'new' && is_array($result['parentPageRow'])) {
$effectivePid = $result['parentPageRow']['uid'];
}
$result['effectivePid'] = (int)$effectivePid;
return $result;
}
}
@@ -0,0 +1,136 @@
<?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 TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
use TYPO3\CMS\Backend\Form\Exception\DatabaseDefaultLanguageException;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Add language related data to result array
*/
readonly class DatabaseLanguageRows implements FormDataProviderInterface
{
public function __construct(
private TranslationConfigurationProvider $translationConfigurationProvider,
) {}
/**
* Fetch default language if handled record is a localized one,
* unserialize transOrigDiffSourceField if it is defined,
* fetch additional languages if requested.
*
* @return array
* @throws DatabaseDefaultLanguageException
*/
public function addData(array $result)
{
if (!empty($result['processedTca']['ctrl']['languageField'])
&& !empty($result['processedTca']['ctrl']['transOrigPointerField'])
) {
$languageField = $result['processedTca']['ctrl']['languageField'];
$fieldWithUidOfDefaultRecord = $result['processedTca']['ctrl']['transOrigPointerField'];
if (isset($result['databaseRow'][$languageField]) && $result['databaseRow'][$languageField] > 0
&& isset($result['databaseRow'][$fieldWithUidOfDefaultRecord]) && $result['databaseRow'][$fieldWithUidOfDefaultRecord] > 0
) {
// Default language record of localized record
$defaultLanguageRow = $this->getRecordWorkspaceOverlay(
$result['tableName'],
(int)$result['databaseRow'][$fieldWithUidOfDefaultRecord]
);
if (empty($defaultLanguageRow)) {
throw new DatabaseDefaultLanguageException(
'Default language record with id ' . (int)$result['databaseRow'][$fieldWithUidOfDefaultRecord]
. ' not found in table ' . $result['tableName'] . ' while editing record ' . $result['databaseRow']['uid'],
1438249426
);
}
$result['defaultLanguageRow'] = $defaultLanguageRow;
// Unserialize the "original diff source" if given
if (!empty($result['processedTca']['ctrl']['transOrigDiffSourceField'])
&& !empty($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']])
) {
$defaultLanguageKey = $result['tableName'] . ':' . (int)$result['databaseRow']['uid'];
$result['defaultLanguageDiffRow'][$defaultLanguageKey] = json_decode(
(string)$result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']],
true
);
}
// Add language overlays from further localizations if requested
// @todo: Permission check if user is in "restrict ot language" is missing here.
// @todo: The TranslationConfigurationProvider is more stupid than good for us ... invent a better translation overlay api!
if (!empty($result['userTsConfig']['options.']['additionalPreviewLanguages'])) {
$additionalLanguageUids = GeneralUtility::intExplode(',', (string)$result['userTsConfig']['options.']['additionalPreviewLanguages'], true);
foreach ($additionalLanguageUids as $additionalLanguageUid) {
// Continue if this system language record does not exist or if 0 or -1 is requested
// or if row is the same as the to-be-displayed row
if ($additionalLanguageUid <= 0
|| !isset($result['systemLanguageRows'][$additionalLanguageUid])
|| $additionalLanguageUid === (int)$result['databaseRow'][$languageField]
) {
continue;
}
$translationInfo = $this->translationConfigurationProvider->translationInfo(
$result['tableName'],
(int)$result['databaseRow'][$fieldWithUidOfDefaultRecord],
$additionalLanguageUid
);
if (!empty($translationInfo['translations'][$additionalLanguageUid]['uid'])) {
$record = $this->getRecordWorkspaceOverlay(
$result['tableName'],
(int)$translationInfo['translations'][$additionalLanguageUid]['uid']
);
$result['additionalLanguageRows'][$additionalLanguageUid] = $record;
}
}
}
// @todo do that only if l10n_parent > 0 (not in "free mode")?
if (!empty($result['processedTca']['ctrl']['translationSource'])
&& is_string($result['processedTca']['ctrl']['translationSource'])
) {
$translationSourceFieldName = $result['processedTca']['ctrl']['translationSource'];
if (isset($result['databaseRow'][$translationSourceFieldName])
&& $result['databaseRow'][$translationSourceFieldName] > 0
) {
$uidOfTranslationSource = $result['databaseRow'][$translationSourceFieldName];
$result['sourceLanguageRow'] = $this->getRecordWorkspaceOverlay(
$result['tableName'],
$uidOfTranslationSource
);
}
}
}
}
return $result;
}
/**
* Retrieve the requested row from the database
*/
protected function getRecordWorkspaceOverlay(string $tableName, int $uid): array
{
$row = BackendUtility::getRecordWSOL($tableName, $uid);
return $row ?: [];
}
}
@@ -0,0 +1,78 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fill the "pageLanguageOverlayRows" part of the result array
*/
readonly class DatabasePageLanguageOverlayRows implements FormDataProviderInterface
{
public function __construct(
private Context $context,
private ConnectionPool $connectionPool,
) {}
/**
* Fetch available page overlay records of page
*
* @return array
*/
public function addData(array $result)
{
if ($result['effectivePid'] === 0) {
// No overlays for records on pid 0 and not for new pages below root
return $result;
}
$result['pageLanguageOverlayRows'] = $this->getDatabaseRows($result['tcaSchemata']->get('pages'), (int)$result['effectivePid']);
return $result;
}
/**
* Retrieve the requested overlay row from the database
*/
protected function getDatabaseRows(TcaSchema $pageSchema, int $pid): array
{
$workspaceId = $this->context->getPropertyFromAspect('workspace', 'id');
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$workspaceId));
$rows = $queryBuilder
->select('*')
->from('pages')
->where($queryBuilder->expr()->eq(
$pageSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName(),
$queryBuilder->createNamedParameter($pid, Connection::PARAM_INT)
))
->executeQuery()
->fetchAllAssociative();
return $rows;
}
}
@@ -0,0 +1,36 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
/**
* Set rootline
*/
class DatabasePageRootline implements FormDataProviderInterface
{
/**
* Fetch rootline
*
* @return array
*/
public function addData(array $result)
{
$result['rootline'] = BackendUtility::BEgetRootLine($result['effectivePid'], '', true);
return $result;
}
}
@@ -0,0 +1,65 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Fetch parent page row from database if possible
*/
class DatabaseParentPageRow extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
/**
* Add parent page row of existing row to result
* parentPageRow will stay NULL in result if a record is added or edited below root node
*
* @return array
*/
public function addData(array $result)
{
// $parentPageRow end up NULL if a record added or edited on root node
$parentPageRow = null;
if ($result['command'] === 'new') {
if (MathUtility::canBeInterpretedAsInteger($result['vanillaUid'])) {
$vanillaUid = (int)$result['vanillaUid'];
if ($vanillaUid < 0) {
// vanillaUid points to a neighbor record in same table - get its record and its pid from there to find parent record
$neighborRow = $this->getRecordFromDatabase($result['tableName'], (int)abs($vanillaUid));
if (!empty($neighborRow['t3ver_oid'])) {
$neighborRow = $this->getRecordFromDatabase($result['tableName'], (int)$neighborRow['t3ver_oid']);
}
$result['neighborRow'] = $neighborRow;
// uid of page the record is located in
$neighborRowPid = (int)$neighborRow['pid'];
if ($neighborRowPid !== 0) {
// Fetch the parent page record only if it is not the '0' root
$parentPageRow = $this->getRecordFromDatabase('pages', $neighborRowPid);
}
} elseif ($vanillaUid > 0) {
// vanillaUid points to a page uid directly
$parentPageRow = $this->getRecordFromDatabase('pages', $vanillaUid);
}
}
} elseif ($result['databaseRow']['pid'] > 0) {
// On "edit", the row itself has been fetched already
$parentPageRow = $this->getRecordFromDatabase('pages', (int)$result['databaseRow']['pid']);
}
$result['parentPageRow'] = $parentPageRow;
return $result;
}
}
@@ -0,0 +1,45 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Determine the final TCA type value
*/
class DatabaseRecordOverrideValues implements FormDataProviderInterface
{
/**
* Add override values to the databaseRow fields. As those values are not meant to
* be overwritten by the user, the TCA of the field is set to type hidden.
*
* @return array
*/
public function addData(array $result)
{
foreach ($result['overrideValues'] as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
$result['processedTca']['columns'][$fieldName]['config'] = [
'type' => 'hidden',
'renderType' => 'hidden',
];
}
}
return $result;
}
}
@@ -0,0 +1,149 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Determine the final TCA type value
*/
class DatabaseRecordTypeValue implements FormDataProviderInterface
{
/**
* TCA type value depends on several parameters. The simple case is
* a direct lookup in the database row, which then just needs handling
* in case the row is a localization overlay.
* More complex is the field:field syntax that can look up the actual
* value in a different table.
*
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
if (!isset($result['processedTca']['types'])
|| !is_array($result['processedTca']['types'])
|| empty($result['processedTca']['types'])
) {
throw new \UnexpectedValueException(
'At least one "types" array must be defined for table ' . $result['tableName'] . ', preferred "0"',
1438185331
);
}
// Guard clause to suppress any calculation if record type value has been set from outside already
if ($result['recordTypeValue'] !== '') {
return $result;
}
$recordTypeValue = '0';
if (!empty($result['processedTca']['ctrl']['type'])) {
$tcaTypeField = $result['processedTca']['ctrl']['type'];
if (!str_contains($tcaTypeField, ':')) {
// $tcaTypeField is the name of a field in database row
if (!array_key_exists($tcaTypeField, $result['databaseRow'])) {
throw new \UnexpectedValueException(
'TCA table ' . $result['tableName'] . ' ctrl[\'type\'] is set to ' . $tcaTypeField . ', but'
. ' this field does not exist in the database of this table',
1438183881
);
}
$recordTypeValue = $result['databaseRow'][$tcaTypeField];
} else {
// If type is configured as localField:foreignField, fetch the type value from
// a foreign table. localField then point to a group or select field in the own table,
// this points to a record in a foreign table and the value of foreignField is then
// used as type field. This was introduced for some FAL scenarios.
[$pointerField, $foreignTableTypeField] = explode(':', $tcaTypeField);
$relationType = (string)($result['processedTca']['columns'][$pointerField]['config']['type'] ?? '');
if (!in_array($relationType, ['select', 'category', 'group'], true)) {
throw new \UnexpectedValueException(
'TCA foreign field pointer fields are only allowed to be used with group, select or category field types.'
. ' Handling field ' . $pointerField . ' with type configured as ' . $tcaTypeField,
1325862241
);
}
$foreignUid = $result['databaseRow'][$pointerField];
// Resolve the foreign record only if there is a uid, otherwise fall back 0
if (!empty($foreignUid)) {
// Determine table name to fetch record from
if ($relationType === 'select' || $relationType === 'category') {
$foreignTable = $result['processedTca']['columns'][$pointerField]['config']['foreign_table'] ?? '';
} else {
$allowedTables = explode(',', $result['processedTca']['columns'][$pointerField]['config']['allowed']);
// Always take the first configured table.
$foreignTable = $allowedTables[0];
}
if (empty($foreignTable)) {
throw new \UnexpectedValueException(
'No target table defined for type config field ' . $pointerField . ' of table ' . $result['tableName'],
1438253614
);
}
if (!MathUtility::canBeInterpretedAsInteger($foreignUid) && is_array($foreignUid[0])) {
// A group relation - has been resolved to array by TcaGroup data provider already
$foreignUid = $foreignUid[0]['uid'];
}
// Fetch field of this foreign row from db
if (MathUtility::canBeInterpretedAsInteger($foreignUid)) {
$foreignRow = $this->getDatabaseRow($foreignTable, (int)$foreignUid, $foreignTableTypeField);
if (!empty($foreignRow[$foreignTableTypeField])) {
// @todo: It might be necessary to fetch the value from default language record as well here,
// @todo: this was buggy in the "old" implementation and never worked. It was therefor left out here for now.
// @todo: To implement that, see if the foreign row is a localized overlay, fetch default and merge exclude
$recordTypeValue = $foreignRow[$foreignTableTypeField];
}
}
}
}
}
// Throw another exception if determined value and '0' and '1' do not exist
if (empty($result['processedTca']['types'][$recordTypeValue])
&& empty($result['processedTca']['types']['0'])
&& empty($result['processedTca']['types']['1'])
) {
throw new \UnexpectedValueException(
'Type value ' . $recordTypeValue . ' from database record not defined in TCA of table '
. $result['tableName'] . ' and neither 0 nor 1 are defined as fallback.',
1438185437
);
}
// Check the determined value actually exists as types key, otherwise fall back to 0 or 1, 1 for "historical reasons"
if (empty($result['processedTca']['types'][$recordTypeValue])) {
$recordTypeValue = !empty($result['processedTca']['types']['0']) ? '0' : '1';
}
$result['recordTypeValue'] = (string)$recordTypeValue;
return $result;
}
/**
* Retrieve the requested row from the database
*/
protected function getDatabaseRow(string $tableName, int $uid, string $fieldName): array
{
$row = BackendUtility::getRecord($tableName, $uid, $fieldName);
return $row ?: [];
}
}
@@ -0,0 +1,44 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Domain\DateTimeFactory;
/**
* Migrate type=datetime field values to \DateTimeImmutable
*/
class DatabaseRowDateTimeFields implements FormDataProviderInterface
{
public function addData(array $result): array
{
foreach ($result['processedTca']['columns'] as $column => $columnConfig) {
$type = $columnConfig['config']['type'] ?? '';
if ($type !== 'datetime') {
continue;
}
try {
$result['databaseRow'][$column] = DateTimeFactory::createFromDatabaseValueAndTCAConfig(
$result['databaseRow'][$column] ?? null,
$columnConfig['config'] ?? [],
);
} catch (\InvalidArgumentException) {
$result['databaseRow'][$column] = null;
}
}
return $result;
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Special data provider for replacing a database field with the value of
* the default record in case "l10n_display" is set to "defaultAsReadonly".
*/
class DatabaseRowDefaultAsReadonly implements FormDataProviderInterface
{
/**
* Check each field for being an overlay, having l10n_display set to defaultAsReadonly
* and whether the field exists in the default language row. If so, the current
* database value will be replaced by the one from the default language row.
*/
public function addData(array $result): array
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (!isset($result['defaultLanguageRow'][$fieldName])) {
// No default value available for this field
continue;
}
if (!GeneralUtility::inList(($result['processedTca']['columns'][$fieldName]['l10n_display'] ?? ''), 'defaultAsReadonly')) {
// defaultAsReadonly is not set for this field
continue;
}
$languageField = (string)($result['processedTca']['ctrl']['languageField'] ?? '');
$transOrigPointerField = (string)($result['processedTca']['ctrl']['transOrigPointerField'] ?? '');
if ($languageField === '' || $transOrigPointerField === ''
|| !($result['databaseRow'][$languageField] ?? false)
|| !($result['databaseRow'][$transOrigPointerField] ?? false)
) {
// The current record is not an overlay. Note: This check might already have taken place
// while creating the default language row. However, since this field might be set by
// other data providers unintentional, we check this here again to be sure.
continue;
}
if ((int)$result['databaseRow'][$transOrigPointerField] !== (int)$result['defaultLanguageRow']['uid']) {
// The current records "transOrigPointerField" doesn't point to the current default language row
continue;
}
// Override the current database field with the one from the default language
$result['databaseRow'][$fieldName] = $result['defaultLanguageRow'][$fieldName];
}
return $result;
}
}
@@ -0,0 +1,68 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Handle TCA default values on row. This affects existing rows as well as new rows.
*
* Hint: Even after this class it is NOT safe no rely that *all* fields from
* columns are set in databaseRow.
*/
class DatabaseRowDefaultValues implements FormDataProviderInterface
{
/**
* Initialize new row with default values from various sources
*
* @return array
*/
public function addData(array $result)
{
$databaseRow = $result['databaseRow'];
$newRow = $databaseRow;
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
// Keep current value if it can be resolved to "there is something" directly
if (isset($databaseRow[$fieldName])) {
$newRow[$fieldName] = $databaseRow[$fieldName];
continue;
}
// Special handling for nullable fields
if ($fieldConfig['config']['nullable'] ?? false) {
if (// Field exists and is set to NULL
array_key_exists($fieldName, $databaseRow)
// Default NULL is set, and this is a new record!
|| (array_key_exists('default', $fieldConfig['config']) && $fieldConfig['config']['default'] === null)
) {
$newRow[$fieldName] = null;
} else {
$newRow[$fieldName] = (string)($fieldConfig['config']['default'] ?? '');
}
} else {
// Fun part: This forces empty string for any field even if no default is set. This is
// a useful side effect in flex form section containers where a new field is added to an existing
// value array because it was added to a data structure.
$newRow[$fieldName] = (string)($fieldConfig['config']['default'] ?? '');
}
}
$result['databaseRow'] = $newRow;
return $result;
}
}
@@ -0,0 +1,385 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* On "new" command, initialize new database row with default data
*/
class DatabaseRowInitializeNew implements FormDataProviderInterface
{
/**
* Initialize new row with default values from various sources
* There are 4 sources of default values. Mind the order, the last takes precedence.
*
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
if ($result['command'] !== 'new') {
return $result;
}
if (!is_array($result['databaseRow'])) {
throw new \UnexpectedValueException(
'databaseRow of table ' . $result['tableName'] . ' is not an array',
1444431128
);
}
$result = $this->setDefaultsFromUserTsConfig($result);
$result = $this->setDefaultsFromPageTsConfig($result);
$result = $this->setDefaultsFromNeighborRow($result);
$result = $this->setDefaultsFromDefaultValues($result);
$result = $this->setDefaultsFromInlineRelations($result);
$result = $this->setDefaultsFromInlineParentLanguage($result);
$result = $this->setDefaultsFromInlineParentUid($result);
$result = $this->setPid($result);
return $result;
}
/**
* Set defaults defined by user ts "TCAdefaults"
* Supports both field-level defaults and type-specific defaults
* TCAdefaults.tt_content.header_layout = 1 (field-level)
* TCAdefaults.tt_content.header_layout.types.textmedia = 3 (type-specific)
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromUserTsConfig(array $result)
{
$tableNameWithDot = $result['tableName'] . '.';
// Apply default values from user typo script "TCAdefaults" if any
if (isset($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
&& is_array($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
) {
$recordType = $this->getRecordTypeFromRow($result);
$mergedDefaults = $this->mergeTypeSpecificTcaDefaults(
$result['userTsConfig']['TCAdefaults.'][$tableNameWithDot],
$recordType
);
foreach ($mergedDefaults as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
}
}
}
return $result;
}
/**
* Set defaults defined by page ts "TCAdefaults"
* Supports both field-level defaults and type-specific defaults
* TCAdefaults.tt_content.header_layout = 1 (field-level)
* TCAdefaults.tt_content.header_layout.types.textmedia = 3 (type-specific)
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromPageTsConfig(array $result)
{
$tableNameWithDot = $result['tableName'] . '.';
if (isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
&& is_array($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
) {
$recordType = $this->getRecordTypeFromRow($result);
$mergedDefaults = $this->mergeTypeSpecificTcaDefaults(
$result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot],
$recordType
);
foreach ($mergedDefaults as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
}
}
}
return $result;
}
/**
* If a neighbor row is given (if vanillaUid was negative), field can be initialized with values
* from neighbor for fields registered in TCA['ctrl']['useColumnsForDefaultValues'].
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromNeighborRow(array $result)
{
if (is_array($result['neighborRow'])
&& !empty($result['processedTca']['ctrl']['useColumnsForDefaultValues'])
) {
$defaultColumns = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['useColumnsForDefaultValues'], true);
foreach ($defaultColumns as $fieldName) {
if (isset($result['processedTca']['columns'][$fieldName])
&& isset($result['neighborRow'][$fieldName])
) {
$result['databaseRow'][$fieldName] = $result['neighborRow'][$fieldName];
}
}
}
return $result;
}
/**
* Apply default values.
* These are typically carried around as "defVals" GET vars and set by controllers
* in $result['defaultValues'] array as init values.
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDefaultsFromDefaultValues(array $result)
{
$tableName = $result['tableName'];
$defaultValues = $result['defaultValues'] ?? [];
if (isset($defaultValues[$tableName]) && is_array($defaultValues[$tableName])) {
foreach ($defaultValues[$tableName] as $fieldName => $fieldValue) {
if (isset($result['processedTca']['columns'][$fieldName])) {
$result['databaseRow'][$fieldName] = $fieldValue;
}
}
}
return $result;
}
/**
* Inline scenario if a new intermediate record to an existing child-child is
* compiled. Set "foreign_selector" field of this intermediate row to given
* "childChildUid". See TcaDataCompiler array comment of inlineChildChildUid
* for more details.
*
* @param array $result Result array
* @return array Modified result array
* @throws \UnexpectedValueException
*/
protected function setDefaultsFromInlineRelations(array $result)
{
if ($result['inlineChildChildUid'] === null) {
return $result;
}
if (!is_int($result['inlineChildChildUid'])) {
throw new \UnexpectedValueException(
'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but is not an integer',
1444434103
);
}
if (!isset($result['inlineParentConfig']['foreign_selector'])) {
throw new \UnexpectedValueException(
'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but no foreign_selector in inlineParentConfig',
1444434102
);
}
$selectorFieldName = $result['inlineParentConfig']['foreign_selector'];
$fieldType = (string)($result['processedTca']['columns'][$selectorFieldName]['config']['type'] ?? '');
if (!in_array($fieldType, ['select', 'category', 'group'], true)) {
throw new \UnexpectedValueException(
$selectorFieldName . ' is target type of a foreign_selector field to table ' . $result['tableName'] . ' and must be either a select, category or group type field',
1444434104
);
}
if ($result['inlineChildChildUid']) {
$result['databaseRow'][$selectorFieldName] = $result['inlineChildChildUid'];
}
return $result;
}
/**
* If a new child is created in an inline relation via ajax, and if the parent is a localized record,
* the child should have the same sys_language_uid set in the field declared in ['ctrl']['languageField']
* if the child is localizable itself.
* A localized parent transfers its sys_language_uid via inlineParentConfig['inline']['parentSysLanguageUid'],
* use that value as default for the child record languageField.
*
* @param array $result Result array
* @return array Modified result array
* @throws \UnexpectedValueException
*/
protected function setDefaultsFromInlineParentLanguage(array $result): array
{
if (!isset($result['inlineParentConfig']['inline']['parentSysLanguageUid'])
|| empty($result['processedTca']['ctrl']['languageField'])
|| empty($result['processedTca']['ctrl']['transOrigPointerField'])
) {
return $result;
}
if (!MathUtility::canBeInterpretedAsInteger($result['inlineParentConfig']['inline']['parentSysLanguageUid'])) {
throw new \UnexpectedValueException(
'A sys_language_uid is set from inline parent config but the value is no integer',
1490360772
);
}
$parentSysLanguageUid = (int)$result['inlineParentConfig']['inline']['parentSysLanguageUid'];
$languageFieldName = $result['processedTca']['ctrl']['languageField'];
$result['databaseRow'][$languageFieldName] = $parentSysLanguageUid;
return $result;
}
/**
* Set the parent uid of inline relations created via ajax to the corresponding foreign field
*
* @param array $result Result array
*/
protected function setDefaultsFromInlineParentUid(array $result): array
{
$isInlineChild = $result['isInlineChild'] ?? false;
$parentField = $result['inlineParentConfig']['foreign_field'] ?? false;
if ($isInlineChild && $parentField && !empty($result['inlineParentUid'])) {
$result['databaseRow'][$parentField] = $result['inlineParentUid'];
}
return $result;
}
/**
* Set the pid. This is either the vanillaUid (see description in FormDataCompiler),
* or a pid given by page TSconfig for inline children.
*
* @param array $result Result array
* @return array Modified result array
* @throws \UnexpectedValueException
*/
protected function setPid(array $result)
{
// Set pid to vanillaUid. This can be a negative value
// if the record is added relative to another record.
$result['databaseRow']['pid'] = $result['vanillaUid'];
// In case a new inline record is created, the pid can be set to a different value
// by page TSconfig, but not by user TSconfig. This overrides the above pid selection
// and forces the pid of new inline children.
$tableNameWithDot = $result['tableName'] . '.';
if ($result['isInlineChild'] && isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'])) {
if (!MathUtility::canBeInterpretedAsInteger($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'])) {
throw new \UnexpectedValueException(
'page TSconfig setting TCAdefaults.' . $tableNameWithDot . 'pid must be a number, but given string '
. $result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'] . ' can not be interpreted as integer',
1461598332
);
}
$result['databaseRow']['pid'] = (int)$result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot]['pid'];
}
return $result;
}
/**
* Determine the record type from the current database row
* and additionally fall back to defaultValues as this is
* basically the only source we have for new records at this point.
*
* @param array $result The form data result array
* @return string The record type value
*/
protected function getRecordTypeFromRow(array $result): string
{
// If recordTypeValue is already set, use it
if (is_string($result['recordTypeValue'] ?? false) && $result['recordTypeValue'] !== '') {
return $result['recordTypeValue'];
}
$recordTypeValue = '0';
// Check if there's a type field defined in TCA
if (is_string($result['processedTca']['ctrl']['type'] ?? false) && $result['processedTca']['ctrl']['type'] !== '') {
$tcaTypeField = $result['processedTca']['ctrl']['type'];
// Handle simple type field (not foreign field reference)
if (!str_contains($tcaTypeField, ':')) {
// First check if the type field exists in the database row and has a value
if (array_key_exists($tcaTypeField, $result['databaseRow']) && $result['databaseRow'][$tcaTypeField] !== null) {
$recordTypeValue = (string)$result['databaseRow'][$tcaTypeField];
} else {
// Check if the type field is set in defaultValues
$tableName = $result['tableName'];
if (isset($result['defaultValues'][$tableName][$tcaTypeField])) {
$recordTypeValue = (string)$result['defaultValues'][$tableName][$tcaTypeField];
}
}
}
}
// Apply the same fallback logic as DatabaseRecordTypeValue:
// Check the determined value actually exists as types key, otherwise fall back to 0 or 1
if (empty($result['processedTca']['types'][$recordTypeValue])) {
$recordTypeValue = !empty($result['processedTca']['types']['0']) ? '0' : '1';
}
return $recordTypeValue;
}
/**
* Merge type-specific TCAdefaults over field-level defaults
* Similar to how TCEFORM type-specific settings work
*
* @param array $tcaDefaults The TCAdefaults configuration for the table
* @param string $recordType The current record type value
* @return array Merged defaults with type-specific values taking precedence
*/
protected function mergeTypeSpecificTcaDefaults(array $tcaDefaults, string $recordType): array
{
$mergedDefaults = [];
foreach ($tcaDefaults as $fieldKey => $fieldConfiguration) {
if (str_ends_with($fieldKey, '.')) {
// This is a field with sub-configuration (potentially types)
$fieldName = rtrim($fieldKey, '.');
if (!is_array($fieldConfiguration)) {
continue;
}
$fieldDefault = $fieldConfiguration;
// Check if there are type-specific overrides
if (!empty($fieldConfiguration['types.']) && is_array($fieldConfiguration['types.'])) {
$typeSpecificConfiguration = $fieldConfiguration['types.'];
unset($fieldDefault['types.']);
// If we have a matching type-specific configuration, merge it
if (!empty($typeSpecificConfiguration[$recordType . '.']) && is_array($typeSpecificConfiguration[$recordType . '.'])) {
ArrayUtility::mergeRecursiveWithOverrule($fieldDefault, $typeSpecificConfiguration[$recordType . '.']);
} elseif (!empty($typeSpecificConfiguration[$recordType])) {
// Simple value (not array)
$mergedDefaults[$fieldName] = $typeSpecificConfiguration[$recordType];
continue;
}
}
// If the field configuration is now empty (only had types.), skip it
// Otherwise, use the first non-array value or the whole configuration
foreach ($fieldDefault as $key => $value) {
if (!str_ends_with($key, '.') && !is_array($value)) {
$mergedDefaults[$fieldName] = $value;
break;
}
}
} else {
// Simple field-level default
$mergedDefaults[$fieldKey] = $fieldConfiguration;
}
}
return $mergedDefaults;
}
}
@@ -0,0 +1,104 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
/**
* Fill the "systemLanguageRows" part of the result array
*/
readonly class DatabaseSystemLanguageRows implements FormDataProviderInterface
{
public function __construct(
private FlashMessageService $flashMessageService
) {}
/**
* Fetch available system languages and resolve iso code if necessary.
*
* @return array
* @throws \LogicException
*/
public function addData(array $result)
{
$site = $result['site'] ?? null;
if (!$site instanceof SiteInterface) {
throw new \LogicException(
'No valid site object found in $result[\'site\']',
1534952559
);
}
$pageIdDefaultLanguage = $result['defaultLanguagePageRow']['uid'] ?? $result['effectivePid'];
$languages = $site->getAvailableLanguages($this->getBackendUser(), true, $pageIdDefaultLanguage);
$languageRows = [];
foreach ($languages as $language) {
$languageId = $language->getLanguageId();
if ($languageId > 0) {
$iso = $language->getLocale()->getLanguageCode();
} else {
$iso = 'DEF';
}
$languageRows[$languageId] = [
'uid' => $languageId,
'title' => $language->getTitle(),
'iso' => $iso,
'flagIconIdentifier' => $language->getFlagIdentifier(),
];
if (empty($iso)) {
// No iso code could be found. This is currently possible in the system but discouraged.
// So, code within FormEngine has to be suited to work with an empty iso code. However,
// it may impact certain multi language scenarios, so we add a flash message hinting for
// incomplete configuration here.
// It might be possible to convert this to a non-catchable exception later if
// it iso code is enforced on a different layer of the system (tca required + migration wizard).
// @todo: This could be relaxed again if flex form language handling is extracted,
// @todo: since the rest of the FormEngine code does not rely on iso code?
$message = sprintf(
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:error.missingLanguageIsocode'),
$language->getLocale()->getLanguageCode(),
$languageId
);
$flashMessage = new FlashMessage(
$message,
'',
ContextualFeedbackSeverity::ERROR
);
$defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
}
}
$result['systemLanguageRows'] = $languageRows;
return $result;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,56 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* On "new" command, initialize uid with a unique uid
*/
class DatabaseUniqueUidNewRow implements FormDataProviderInterface
{
/**
* Initialize new row with unique uid
*
* @return array
* @throws \InvalidArgumentException
*/
public function addData(array $result)
{
if ($result['command'] !== 'new') {
return $result;
}
// Throw exception if uid is already set and does not start with NEW.
// In some situations a new record needs to be created again so the initialization of default
// values is triggered, but the "ID" of the new record is already known: This is the case if a
// new section container element is added by FormFlexAjaxController to a not yet persisted record.
// In this case, command "new" is given to the data compiler, but the "NEW1234" id has been calculated
// by the former compiler when opening the record already. The ajax controller then hands in the
// "new" command together with the id calculated by the first call.
if (isset($result['databaseRow']['uid']) && !str_starts_with($result['databaseRow']['uid'], 'NEW')) {
throw new \InvalidArgumentException(
'uid is already set to ' . $result['databaseRow']['uid'] . ' and does not start with NEW for a "new" command',
1437991120
);
}
if (!isset($result['databaseRow']['uid'])) {
$result['databaseRow']['uid'] = StringUtility::getUniqueId('NEW');
}
return $result;
}
}
@@ -0,0 +1,190 @@
<?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 Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\Form\Event\ModifyEditFormUserAccessEvent;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedContentEditException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedEditInternalsException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedListenerException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedPageEditException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedPageNewException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedRootNodeException;
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedTableModifyException;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
/**
* Determine user permission for action and check them
*/
readonly class DatabaseUserPermissionCheck implements FormDataProviderInterface
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
) {}
/**
* Set userPermissionOnPage to result array and check access rights.
*
* A couple of different exceptions are thrown here:
* * If something weird happens a top level SPL exception is thrown.
* This indicates a non-recoverable error.
* * If user has no access to whatever should be done, an exception that
* extends from Form\Exception\AccessDeniedException is thrown. This
* can be caught by upper level controller code and can be translated
* to a specific error message that is shown to the user.
*
* @throws AccessDeniedException
*/
public function addData(array $result): array
{
$backendUser = $this->getBackendUser();
// Early return for admins
if ($backendUser->isAdmin()) {
$result['userPermissionOnPage'] = Permission::ALL;
return $result;
}
if (!$backendUser->check('tables_modify', $result['tableName'])) {
// If user has no modify rights on table, processing is stopped by throwing an
// exception immediately. This case can not be circumvented by hooks.
throw new AccessDeniedTableModifyException(
'No table modify permission for user ' . $backendUser->user['uid'] . ' on table ' . $result['tableName'],
1437683248
);
}
$exception = null;
$userPermissionOnPage = new Permission(Permission::NOTHING);
$rootLevelCapability = $result['tcaSchemata']->get($result['tableName'])->getCapability(TcaSchemaCapability::RestrictionRootLevel);
if ($result['command'] === 'new') {
// A new record is created. Access rights of parent record are important here
// @todo: In case of new inline child, parentPageRow should probably be the
// @todo: "inlineFirstPid" page - Maybe effectivePid and parentPageRow should be calculated differently then?
if (is_array($result['parentPageRow'])) {
// Record is added below an existing page
$userPermissionOnPage = new Permission($backendUser->calcPerms($result['parentPageRow']));
if ($result['tableName'] === 'pages') {
// New page is created, user needs PAGE_NEW for this
if (!$userPermissionOnPage->createPagePermissionIsGranted()) {
$exception = new AccessDeniedPageNewException(
'No page new permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['databaseRow']['uid'],
1437745640
);
}
} elseif (!$userPermissionOnPage->editContentPermissionIsGranted()) {
// A regular record is added, not a page. User needs CONTENT_EDIT permission
$exception = new AccessDeniedContentEditException(
'No content new permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['parentPageRow']['uid'],
1437745759
);
}
} elseif ($rootLevelCapability->shallIgnoreRootLevelRestriction()) {
// Non admin is creating a record on root node for a table that is actively allowed
$userPermissionOnPage->set(Permission::ALL);
} else {
// Non admin has no create permission on root node records
$exception = new AccessDeniedRootNodeException(
'No record creation permission for user ' . $backendUser->user['uid'] . ' on page root node',
1437745221
);
}
} else {
// A page or a record on a page is edited
if ($result['tableName'] === 'pages') {
// A page record is edited, check edit rights of this record directly
$userPermissionOnPage = new Permission($backendUser->calcPerms($result['defaultLanguagePageRow'] ?? $result['databaseRow']));
if (!$userPermissionOnPage->editPagePermissionIsGranted()
|| !$backendUser->check('pagetypes_select', $result['databaseRow'][$result['processedTca']['ctrl']['type']])
) {
$exception = new AccessDeniedPageEditException(
'No page edit permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['databaseRow']['uid'],
1437679336
);
}
} elseif (isset($result['parentPageRow']) && is_array($result['parentPageRow'])) {
// A non page record is edited.
// If there is a parent page row, check content edit right of user
$userPermissionOnPage = new Permission($backendUser->calcPerms($result['parentPageRow']));
if (!$userPermissionOnPage->editContentPermissionIsGranted()) {
$exception = new AccessDeniedContentEditException(
'No content edit permission for user ' . $backendUser->user['uid'] . ' on page ' . $result['parentPageRow']['uid'],
1437679657
);
}
} elseif ($rootLevelCapability->shallIgnoreRootLevelRestriction()) {
// Non admin is editing a record on root node for a table that is actively allowed
$userPermissionOnPage->set(Permission::ALL);
} else {
// Non admin has no edit permission on root node records
// @todo: This probably needs further handling, see http://review.typo3.org/40835
$exception = new AccessDeniedRootNodeException(
'No content edit permission for user ' . $backendUser->user['uid'] . ' on page root node',
1437679856
);
}
// If general access is allowed, check record edit access
if ($exception === null
&& !($result['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)
) {
$accessResult = $backendUser->checkRecordEditAccess($result['tableName'], $result['databaseRow']);
if (!$accessResult->isAllowed) {
$exception = new AccessDeniedEditInternalsException($accessResult->errorMessage, 1437687404);
}
}
}
$userHasAccess = $this->eventDispatcher->dispatch(
new ModifyEditFormUserAccessEvent(
$exception,
$result['tableName'],
$result['command'],
$result['databaseRow'],
)
)->doesUserHaveAccess();
// Throw specific exception because a listener to the Event denied the previous positive user access decision
if ($exception === null && !$userHasAccess) {
$exception = new AccessDeniedListenerException(
'Access to table ' . $result['tableName'] . ' for user ' . $backendUser->user['uid'] . ' was denied by a ModifyRecordEditUserAccessEvent listener',
1662727149
);
}
// Unset a previous exception because a listener to the Event allowed the previous negative user access decision
if ($exception !== null && $userHasAccess) {
$exception = null;
}
if ($exception) {
throw $exception;
}
$result['userPermissionOnPage'] = $userPermissionOnPage->__toInt();
return $result;
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,934 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Class implements the TCA 'displayCond' option.
* The display condition is a colon separated string which describes
* the condition to decide whether a form field should be displayed.
*/
class EvaluateDisplayConditions implements FormDataProviderInterface
{
/**
* Remove fields from processedTca columns that should not be displayed.
*
* Strategy of the parser is to first find all displayCond in given tca
* and within all type=flex fields to parse them into an array. This condition
* array contains all information to evaluate that condition in a second
* step that - depending on evaluation result - then throws away or keeps the field.
*
* @param array $result
*/
public function addData(array $result): array
{
$result = $this->parseDisplayConditions($result);
$result = $this->evaluateConditions($result);
return $result;
}
/**
* Find all 'displayCond' in TCA and flex forms and substitute them with an
* array representation that contains all relevant data to
* evaluate the condition later. For "FIELD" conditions the helper methods
* findFieldValue() is used to find the value of the referenced field to put
* that value into the returned array, too. This is important since the referenced
* field is "relative" to the position of the field that has the display condition.
* For instance, "FIELD:aField:=:foo" within a flex form field references a field
* value from the same sheet, and there are many more complex scenarios to resolve.
*
* @param array $result Incoming result array
* @throws \RuntimeException
* @return array Modified result array with all displayCond parsed into arrays
*/
protected function parseDisplayConditions(array $result): array
{
$flexColumns = [];
foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
if (isset($columnConfiguration['config']['type']) && $columnConfiguration['config']['type'] === 'flex') {
$flexColumns[$columnName] = $columnConfiguration;
}
if (!isset($columnConfiguration['displayCond'])) {
continue;
}
$result['processedTca']['columns'][$columnName]['displayCond'] = $this->parseConditionRecursive(
$columnConfiguration['displayCond'],
$result['databaseRow']
);
}
foreach ($flexColumns as $columnName => $flexColumn) {
$sheetNameFieldNames = [];
foreach ($flexColumn['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
// Create a list of all sheet names with field names combinations for later 'sheetName.fieldName' lookups
// 'one.sheet.one.field' as key, with array of "sheetName" and "fieldName" as value
if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) {
foreach ($sheetConfiguration['ROOT']['el'] as $flexElementName => $flexElementConfiguration) {
// section container have no value in its own
if (isset($flexElementConfiguration['type']) && $flexElementConfiguration['type'] === 'array'
&& isset($flexElementConfiguration['section']) && $flexElementConfiguration['section'] == 1
) {
continue;
}
$combinedKey = $sheetName . '.' . $flexElementName;
if (array_key_exists($combinedKey, $sheetNameFieldNames)) {
throw new \RuntimeException(
'Ambiguous sheet name and field name combination: Sheet "' . $sheetNameFieldNames[$combinedKey]['sheetName']
. '" with field name "' . $sheetNameFieldNames[$combinedKey]['fieldName'] . '" overlaps with sheet "'
. $sheetName . '" and field name "' . $flexElementName . '". Do not do that.',
1481483061
);
}
$sheetNameFieldNames[$combinedKey] = [
'sheetName' => $sheetName,
'fieldName' => $flexElementName,
];
}
}
}
foreach ($flexColumn['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
if (isset($sheetConfiguration['ROOT']['displayCond'])) {
// Condition on a flex sheet
$flexContext = [
'context' => 'flexSheet',
'sheetNameFieldNames' => $sheetNameFieldNames,
'currentSheetName' => $sheetName,
'flexFormRowData' => $result['databaseRow'][$columnName] ?? null,
];
$parsedDisplayCondition = $this->parseConditionRecursive(
$sheetConfiguration['ROOT']['displayCond'],
$result['databaseRow'],
$flexContext
);
$result['processedTca']['columns'][$columnName]['config']['ds']
['sheets'][$sheetName]['ROOT']['displayCond']
= $parsedDisplayCondition;
}
if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) {
foreach ($sheetConfiguration['ROOT']['el'] as $flexElementName => $flexElementConfiguration) {
if (isset($flexElementConfiguration['displayCond'])) {
// Condition on a flex element
$flexContext = [
'context' => 'flexField',
'sheetNameFieldNames' => $sheetNameFieldNames,
'currentSheetName' => $sheetName,
'currentFieldName' => $flexElementName,
'flexFormDataStructure' => $result['processedTca']['columns'][$columnName]['config']['ds'],
'flexFormRowData' => $result['databaseRow'][$columnName] ?? null,
];
$parsedDisplayCondition = $this->parseConditionRecursive(
$flexElementConfiguration['displayCond'],
$result['databaseRow'],
$flexContext
);
$result['processedTca']['columns'][$columnName]['config']['ds']
['sheets'][$sheetName]['ROOT']
['el'][$flexElementName]['displayCond']
= $parsedDisplayCondition;
}
if (isset($flexElementConfiguration['type']) && $flexElementConfiguration['type'] === 'array'
&& isset($flexElementConfiguration['section']) && $flexElementConfiguration['section'] == 1
&& isset($flexElementConfiguration['children']) && is_array($flexElementConfiguration['children'])
) {
// Conditions on flex container section elements
foreach ($flexElementConfiguration['children'] as $containerIdentifier => $containerElements) {
if (isset($containerElements['el']) && is_array($containerElements['el'])) {
foreach ($containerElements['el'] as $containerElementName => $containerElementConfiguration) {
if (isset($containerElementConfiguration['displayCond'])) {
$flexContext = [
'context' => 'flexContainerElement',
'sheetNameFieldNames' => $sheetNameFieldNames,
'currentSheetName' => $sheetName,
'currentFieldName' => $flexElementName,
'currentContainerIdentifier' => $containerIdentifier,
'currentContainerElementName' => $containerElementName,
'flexFormDataStructure' => $result['processedTca']['columns'][$columnName]['config']['ds'],
'flexFormRowData' => $result['databaseRow'][$columnName],
];
$parsedDisplayCondition = $this->parseConditionRecursive(
$containerElementConfiguration['displayCond'],
$result['databaseRow'],
$flexContext
);
$result['processedTca']['columns'][$columnName]['config']['ds']
['sheets'][$sheetName]['ROOT']
['el'][$flexElementName]
['children'][$containerIdentifier]
['el'][$containerElementName]['displayCond']
= $parsedDisplayCondition;
}
}
}
}
}
}
}
}
}
return $result;
}
/**
* Parse a condition into an array representation and validate syntax. Handles nested conditions combined with AND and OR.
* Calls itself recursive for nesting and logically combined conditions.
*
* @param mixed $condition Either an array with multiple conditions combined with AND or OR, or a single condition string
* @param array $databaseRow Incoming full database row
* @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions
* @throws \RuntimeException
* @return array Array representation of that condition, see unit tests for details on syntax
*/
protected function parseConditionRecursive($condition, array $databaseRow, array $flexContext = []): array
{
$conditionArray = [];
if (is_string($condition)) {
$conditionArray = $this->parseSingleConditionString($condition, $databaseRow, $flexContext);
} elseif (is_array($condition)) {
foreach ($condition as $logicalOperator => $groupedDisplayConditions) {
$logicalOperator = strtoupper(is_string($logicalOperator) ? $logicalOperator : '');
if (($logicalOperator !== 'AND' && $logicalOperator !== 'OR') || !is_array($groupedDisplayConditions)) {
throw new \RuntimeException(
'Multiple conditions must have boolean operator "OR" or "AND", "' . $logicalOperator . '" given.',
1481380393
);
}
$conditionArray = [
'type' => $logicalOperator,
'subConditions' => [],
];
foreach ($groupedDisplayConditions as $key => $singleDisplayCondition) {
$key = strtoupper((string)$key);
if (($key === 'AND' || $key === 'OR') && is_array($singleDisplayCondition)) {
// Recursion statement: condition is 'AND' or 'OR' and is pointing to an array (should be conditions again)
$conditionArray['subConditions'][] = $this->parseConditionRecursive(
[$key => $singleDisplayCondition],
$databaseRow,
$flexContext
);
} else {
$conditionArray['subConditions'][] = $this->parseConditionRecursive(
$singleDisplayCondition,
$databaseRow,
$flexContext
);
}
}
}
} else {
throw new \RuntimeException(
'Condition must be either an array with sub conditions or a single condition string, type ' . gettype($condition) . ' given.',
1481381058
);
}
return $conditionArray;
}
/**
* Parse a single condition string into pieces, validate them and return
* an array representation.
*
* @param string $conditionString Given condition string like "VERSION:IS:true"
* @param array $databaseRow Incoming full database row
* @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions
* @return array Validated name array, example: [ type="VERSION", isVersion="true" ]
* @throws \RuntimeException
*/
protected function parseSingleConditionString(string $conditionString, array $databaseRow, array $flexContext = []): array
{
$conditionArray = GeneralUtility::trimExplode(':', $conditionString, false, 4);
$namedConditionArray = [
'type' => $conditionArray[0],
];
switch ($namedConditionArray['type']) {
case 'FIELD':
if (empty($conditionArray[1])) {
throw new \RuntimeException(
'Field condition "' . $conditionString . '" must have a field name as second part, none given.'
. 'Example: "FIELD:myField:=:myValue"',
1481385695
);
}
$fieldName = $conditionArray[1];
if (empty($conditionArray[2])) {
throw new \RuntimeException(
'Field condition "' . $conditionString . '" must have a valid operator as third part, none given.',
1481386239
);
}
$namedConditionArray['operator'] = $conditionArray[2];
if (!isset($conditionArray[3])) {
throw new \RuntimeException(
'Field condition "' . $conditionString . '" must have an operand as fourth part, none given.'
. ' Example: "FIELD:myField:=:4"',
1481401543
);
}
$operand = $conditionArray[3];
if ($namedConditionArray['operator'] === 'REQ') {
$operand = strtolower($operand);
if ($operand === 'true') {
$namedConditionArray['operand'] = true;
} elseif ($operand === 'false') {
$namedConditionArray['operand'] = false;
} else {
throw new \RuntimeException(
'Field condition "' . $conditionString . '" must have "true" or "false" as fourth part.'
. ' Example: "FIELD:myField:REQ:true',
1481401892
);
}
} elseif (in_array($namedConditionArray['operator'], ['>', '<', '>=', '<=', 'BIT', '!BIT'], true)) {
if (!MathUtility::canBeInterpretedAsInteger($operand)) {
throw new \RuntimeException(
'Field condition "' . $conditionString . '" with comparison operator ' . $namedConditionArray['operator']
. ' must have a number as fourth part, ' . $operand . ' given. Example: "FIELD:myField:>:42"',
1481456806
);
}
$namedConditionArray['operand'] = (int)$operand;
} elseif (in_array($namedConditionArray['operator'], ['-', '!-'], true)) {
[$minimum, $maximum] = GeneralUtility::trimExplode('-', $operand);
if (!MathUtility::canBeInterpretedAsInteger($minimum) || !MathUtility::canBeInterpretedAsInteger($maximum)) {
throw new \RuntimeException(
'Field condition "' . $conditionString . '" with comparison operator ' . $namedConditionArray['operator']
. ' must have two numbers as fourth part, separated by dash, ' . $operand . ' given. Example: "FIELD:myField:-:1-3"',
1481457277
);
}
$namedConditionArray['operand'] = '';
$namedConditionArray['min'] = (int)$minimum;
$namedConditionArray['max'] = (int)$maximum;
} elseif (in_array($namedConditionArray['operator'], ['IN', '!IN', '=', '!='], true)) {
$namedConditionArray['operand'] = $operand;
} else {
$allowedOperators = ['REQ', '>', '<', '>=', '<=', '-', '!-', '=', '!=', 'IN', '!IN', 'BIT', '!BIT'];
throw new \RuntimeException(
'Field condition "' . $conditionString . '" must have a valid operator as third part, invalid one given.'
. ' Valid operators are: "' . implode('", "', $allowedOperators) . '".'
. ' Example: "FIELD:myField:=:4"',
1745918372
);
}
$namedConditionArray['fieldValue'] = $this->findFieldValue($fieldName, $databaseRow, $flexContext);
break;
case 'HIDE_FOR_NON_ADMINS':
break;
case 'REC':
if (empty($conditionArray[1]) || $conditionArray[1] !== 'NEW') {
throw new \RuntimeException(
'Record condition "' . $conditionString . '" must contain "NEW" keyword: either "REC:NEW:true" or "REC:NEW:false"',
1481384784
);
}
if (empty($conditionArray[2])) {
throw new \RuntimeException(
'Record condition "' . $conditionString . '" must have an operand "true" or "false", none given. Example: "REC:NEW:true"',
1481384947
);
}
$operand = strtolower($conditionArray[2]);
if ($operand === 'true') {
$namedConditionArray['isNew'] = true;
} elseif ($operand === 'false') {
$namedConditionArray['isNew'] = false;
} else {
throw new \RuntimeException(
'Record condition "' . $conditionString . '" must have an operand "true" or "false, example "REC:NEW:true", given: ' . $operand,
1481385173
);
}
// Programming error: There must be a uid available, other data providers should have taken care of that already
if (!array_key_exists('uid', $databaseRow)) {
throw new \RuntimeException(
'Required [\'databaseRow\'][\'uid\'] not found in data array',
1481467208
);
}
// May contain "NEW123..."
$namedConditionArray['uid'] = $databaseRow['uid'];
break;
case 'VERSION':
if (empty($conditionArray[1]) || $conditionArray[1] !== 'IS') {
throw new \RuntimeException(
'Version condition "' . $conditionString . '" must contain "IS" keyword: either "VERSION:IS:false" or "VERSION:IS:true"',
1481383660
);
}
if (empty($conditionArray[2])) {
throw new \RuntimeException(
'Version condition "' . $conditionString . '" must have an operand "true" or "false", none given. Example: "VERSION:IS:true',
1481383888
);
}
$operand = strtolower($conditionArray[2]);
if ($operand === 'true') {
$namedConditionArray['isVersion'] = true;
} elseif ($operand === 'false') {
$namedConditionArray['isVersion'] = false;
} else {
throw new \RuntimeException(
'Version condition "' . $conditionString . '" must have a "true" or "false" operand, example "VERSION:IS:true", given: ' . $operand,
1481384123
);
}
// Programming error: There must be a uid available, other data providers should have taken care of that already
if (!array_key_exists('uid', $databaseRow)) {
throw new \RuntimeException(
'Required [\'databaseRow\'][\'uid\'] not found in data array',
1481469854
);
}
$namedConditionArray['uid'] = $databaseRow['uid'];
if (array_key_exists('t3ver_oid', $databaseRow)) {
$namedConditionArray['t3ver_oid'] = $databaseRow['t3ver_oid'];
}
if (array_key_exists('pid', $databaseRow)) {
$namedConditionArray['pid'] = $databaseRow['pid'];
}
if (array_key_exists('_ORIG_pid', $databaseRow)) {
$namedConditionArray['_ORIG_pid'] = $databaseRow['_ORIG_pid'];
}
break;
case 'USER':
if (empty($conditionArray[1])) {
throw new \RuntimeException(
'User function condition "' . $conditionString . '" must have a user function defined a second part, none given.'
. ' Correct format is USER:\My\User\Func->match:more:arguments,'
. ' given: ' . $conditionString,
1481382954
);
}
$namedConditionArray['function'] = $conditionArray[1];
array_shift($conditionArray);
array_shift($conditionArray);
$parameters = count($conditionArray) < 2
? $conditionArray
: array_merge(
[$conditionArray[0]],
GeneralUtility::trimExplode(':', $conditionArray[1])
);
$namedConditionArray['parameters'] = $parameters;
$namedConditionArray['record'] = $databaseRow;
$namedConditionArray['flexContext'] = $flexContext;
break;
default:
throw new \RuntimeException(
'Unknown condition rule type "' . $namedConditionArray['type'] . '" with display condition "' . $conditionString . '".',
1481381950
);
}
return $namedConditionArray;
}
/**
* Find field value the condition refers to for "FIELD:" conditions. For "normal" TCA fields this is the value of
* a "neighbor" field, but in flex form context it can be prepended with a sheet name. The method sorts out the
* details and returns the current field value.
*
* @param string $givenFieldName The full name used in displayCond. Can have sheet names included in flex context
* @param array $databaseRow Incoming database row values
* @param array $flexContext Detailed flex context if display condition is within a flex field, needed to determine field value for "FIELD" conditions
* @throws \RuntimeException
* @return mixed The current field value from database row or a deeper flex form structure field.
*/
protected function findFieldValue(string $givenFieldName, array $databaseRow, array $flexContext = [])
{
$fieldValue = null;
// Early return for "normal" tca fields
if (empty($flexContext)) {
if (array_key_exists($givenFieldName, $databaseRow)) {
$fieldValue = $databaseRow[$givenFieldName];
}
return $fieldValue;
}
if ($flexContext['context'] === 'flexSheet') {
// A display condition on a flex form sheet. Relatively simple: fieldName is either
// "parentRec.fieldName" pointing to a databaseRow field name, or "sheetName.fieldName" pointing
// to a field value from a neighbor field.
if (str_starts_with($givenFieldName, 'parentRec.')) {
$fieldName = substr($givenFieldName, 10);
if (array_key_exists($fieldName, $databaseRow)) {
$fieldValue = $databaseRow[$fieldName];
}
} else {
if (array_key_exists($givenFieldName, $flexContext['sheetNameFieldNames'])) {
if ($flexContext['currentSheetName'] === $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName']) {
throw new \RuntimeException(
'Configuring displayCond to "' . $givenFieldName . '" on flex form sheet "'
. $flexContext['currentSheetName'] . '" referencing a value from the same sheet does not make sense.',
1481485705
);
}
}
$sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'] ?? null;
$fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'] ?? null;
if (!isset($flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'])) {
throw new \RuntimeException(
'Flex form displayCond on sheet "' . $flexContext['currentSheetName'] . '" references field "' . $fieldName
. '" of sheet "' . $sheetName . '", but that field does not exist in current data structure',
1481488492
);
}
$fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'];
}
} elseif ($flexContext['context'] === 'flexField') {
// A display condition on a flex field. Handle "parentRec." similar to sheet conditions,
// get a list of "local" field names and see if they are used as reference, else see if a
// "sheetName.fieldName" field reference is given
if (str_starts_with($givenFieldName, 'parentRec.')) {
$fieldName = substr($givenFieldName, 10);
if (array_key_exists($fieldName, $databaseRow)) {
$fieldValue = $databaseRow[$fieldName];
}
} else {
$listOfLocalFlexFieldNames = array_keys(
$flexContext['flexFormDataStructure']['sheets'][$flexContext['currentSheetName']]['ROOT']['el']
);
if (in_array($givenFieldName, $listOfLocalFlexFieldNames, true)) {
// Condition references field name of the same sheet
$sheetName = $flexContext['currentSheetName'];
if (!isset($flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$givenFieldName]['vDEF'])) {
throw new \RuntimeException(
'Flex form displayCond on field "' . $flexContext['currentFieldName'] . '" on flex form sheet "'
. $flexContext['currentSheetName'] . '" references field "' . $givenFieldName . '", but a field value'
. ' does not exist in this sheet',
1481492953
);
}
$fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$givenFieldName]['vDEF'];
} elseif (in_array($givenFieldName, array_keys($flexContext['sheetNameFieldNames'], true))) {
// Condition references field name including a sheet name
$sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'];
$fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'];
$fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'];
} else {
throw new \RuntimeException(
'Flex form displayCond on field "' . $flexContext['currentFieldName'] . '" on flex form sheet "'
. $flexContext['currentSheetName'] . '" references a field or field / sheet combination "'
. $givenFieldName . '" that might be defined in given data structure but is not found in data values.',
1481496170
);
}
}
} elseif ($flexContext['context'] === 'flexContainerElement') {
// A display condition on a flex form section container element. Handle "parentRec.", compare to a
// list of local field names, compare to a list of field names from same sheet, compare to a list
// of sheet fields from other sheets.
if (str_starts_with($givenFieldName, 'parentRec.')) {
$fieldName = substr($givenFieldName, 10);
if (array_key_exists($fieldName, $databaseRow)) {
$fieldValue = $databaseRow[$fieldName];
}
} else {
$currentSheetName = $flexContext['currentSheetName'];
$currentFieldName = $flexContext['currentFieldName'];
$currentContainerIdentifier = $flexContext['currentContainerIdentifier'];
$currentContainerElementName = $flexContext['currentContainerElementName'];
$listOfLocalContainerElementNames = array_keys(
$flexContext['flexFormDataStructure']['sheets'][$currentSheetName]['ROOT']
['el'][$currentFieldName]
['children'][$currentContainerIdentifier]
['el']
);
$listOfLocalContainerElementNamesWithSheetName = [];
foreach ($listOfLocalContainerElementNames as $aContainerElementName) {
$listOfLocalContainerElementNamesWithSheetName[$currentSheetName . '.' . $aContainerElementName] = [
'containerElementName' => $aContainerElementName,
];
}
$listOfLocalFlexFieldNames = array_keys(
$flexContext['flexFormDataStructure']['sheets'][$currentSheetName]['ROOT']['el']
);
if (in_array($givenFieldName, $listOfLocalContainerElementNames, true)) {
// Condition references field of same container instance
$containerType = current(array_keys(
$flexContext['flexFormRowData']['data'][$currentSheetName]
['lDEF'][$currentFieldName]
['el'][$currentContainerIdentifier]
));
$fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName]
['lDEF'][$currentFieldName]
['el'][$currentContainerIdentifier]
[$containerType]
['el'][$givenFieldName]['vDEF'];
} elseif (in_array($givenFieldName, array_keys($listOfLocalContainerElementNamesWithSheetName, true))) {
// Condition references field name of same container instance and has sheet name included
$containerType = current(array_keys(
$flexContext['flexFormRowData']['data'][$currentSheetName]
['lDEF'][$currentFieldName]
['el'][$currentContainerIdentifier]
));
$fieldName = $listOfLocalContainerElementNamesWithSheetName[$givenFieldName]['containerElementName'];
$fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName]
['lDEF'][$currentFieldName]
['el'][$currentContainerIdentifier]
[$containerType]
['el'][$fieldName]['vDEF'];
} elseif (in_array($givenFieldName, $listOfLocalFlexFieldNames, true)) {
// Condition reference field name of sheet this section container is in
$fieldValue = $flexContext['flexFormRowData']['data'][$currentSheetName]
['lDEF'][$givenFieldName]['vDEF'];
} elseif (in_array($givenFieldName, array_keys($flexContext['sheetNameFieldNames'], true))) {
$sheetName = $flexContext['sheetNameFieldNames'][$givenFieldName]['sheetName'];
$fieldName = $flexContext['sheetNameFieldNames'][$givenFieldName]['fieldName'];
$fieldValue = $flexContext['flexFormRowData']['data'][$sheetName]['lDEF'][$fieldName]['vDEF'];
} else {
$containerType = current(array_keys(
$flexContext['flexFormRowData']['data'][$currentSheetName]
['lDEF'][$currentFieldName]
['el'][$currentContainerIdentifier]
));
throw new \RuntimeException(
'Flex form displayCond on section container field "' . $currentContainerElementName . '" of container type "'
. $containerType . '" on flex form sheet "'
. $flexContext['currentSheetName'] . '" references a field or field / sheet combination "'
. $givenFieldName . '" that might be defined in given data structure but is not found in data values.',
1481634649
);
}
}
}
return $fieldValue;
}
/**
* Loop through TCA, find prepared conditions and evaluate them. Delete either the
* field itself if the condition did not match, or the 'displayCond' in TCA.
*/
protected function evaluateConditions(array $result): array
{
// Evaluate normal tca fields first
$listOfFlexFieldNames = [];
foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
$conditionResult = true;
if (isset($columnConfiguration['displayCond'])) {
$conditionResult = $this->evaluateConditionRecursive($columnConfiguration['displayCond']);
if (!$conditionResult) {
unset($result['processedTca']['columns'][$columnName]);
} else {
// Always unset the whole parsed display condition to save some memory, we're done with them
unset($result['processedTca']['columns'][$columnName]['displayCond']);
}
}
// If field was not removed and if it is a flex field, add to list of flex fields to scan
if ($conditionResult && ($columnConfiguration['config']['type'] ?? false) === 'flex') {
$listOfFlexFieldNames[] = $columnName;
}
}
// Search for flex fields and evaluate sheet conditions throwing them away if needed
foreach ($listOfFlexFieldNames as $columnName) {
$columnConfiguration = $result['processedTca']['columns'][$columnName] ?? [];
foreach ($columnConfiguration['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
if (is_array($sheetConfiguration['ROOT']['displayCond'] ?? false)) {
if (!$this->evaluateConditionRecursive($sheetConfiguration['ROOT']['displayCond'])) {
unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]);
} else {
unset($result['processedTca']['columns'][$columnName]['config']['ds']['sheets'][$sheetName]['ROOT']['displayCond']);
}
}
}
}
// With full sheets gone we loop over display conditions of single fields in flex to throw fields away if needed
$listOfFlexSectionContainers = [];
foreach ($listOfFlexFieldNames as $columnName) {
$columnConfiguration = $result['processedTca']['columns'][$columnName];
if (is_array($columnConfiguration['config']['ds']['sheets'])) {
foreach ($columnConfiguration['config']['ds']['sheets'] as $sheetName => $sheetConfiguration) {
if (isset($sheetConfiguration['ROOT']['el']) && is_array($sheetConfiguration['ROOT']['el'])) {
foreach ($sheetConfiguration['ROOT']['el'] as $flexField => $flexConfiguration) {
$conditionResult = true;
if (isset($flexConfiguration['displayCond']) && is_array($flexConfiguration['displayCond'])) {
$conditionResult = $this->evaluateConditionRecursive($flexConfiguration['displayCond']);
if (!$conditionResult) {
unset(
$result['processedTca']['columns'][$columnName]['config']['ds']
['sheets'][$sheetName]['ROOT']
['el'][$flexField]
);
} else {
unset(
$result['processedTca']['columns'][$columnName]['config']['ds']
['sheets'][$sheetName]['ROOT']
['el'][$flexField]['displayCond']
);
}
}
// If it was not removed and if the field is a section container, add it to the section container list
if ($conditionResult
&& isset($flexConfiguration['type']) && $flexConfiguration['type'] === 'array'
&& isset($flexConfiguration['section']) && $flexConfiguration['section'] == 1
&& isset($flexConfiguration['children']) && is_array($flexConfiguration['children'])
) {
$listOfFlexSectionContainers[] = [
'columnName' => $columnName,
'sheetName' => $sheetName,
'flexField' => $flexField,
];
}
}
}
}
}
}
// Loop over found section container elements and evaluate their conditions
foreach ($listOfFlexSectionContainers as $flexSectionContainerPosition) {
$columnName = $flexSectionContainerPosition['columnName'];
$sheetName = $flexSectionContainerPosition['sheetName'];
$flexField = $flexSectionContainerPosition['flexField'];
$sectionElement = $result['processedTca']['columns'][$columnName]['config']['ds']
['sheets'][$sheetName]['ROOT']
['el'][$flexField];
foreach ($sectionElement['children'] as $containerInstanceName => $containerDataStructure) {
if (isset($containerDataStructure['el']) && is_array($containerDataStructure['el'])) {
foreach ($containerDataStructure['el'] as $containerElementName => $containerElementConfiguration) {
if (isset($containerElementConfiguration['displayCond']) && is_array($containerElementConfiguration['displayCond'])) {
if (!$this->evaluateConditionRecursive($containerElementConfiguration['displayCond'])) {
unset(
$result['processedTca']['columns'][$columnName]['config']['ds']
['sheets'][$sheetName]['ROOT']
['el'][$flexField]
['children'][$containerInstanceName]
['el'][$containerElementName]
);
} else {
unset(
$result['processedTca']['columns'][$columnName]['config']['ds']
['sheets'][$sheetName]['ROOT']
['el'][$flexField]
['children'][$containerInstanceName]
['el'][$containerElementName]['displayCond']
);
}
}
}
}
}
}
return $result;
}
/**
* Evaluate a condition recursive by evaluating the single condition type
*
* @param array $conditionArray The condition to evaluate, possibly with subConditions for AND and OR types
* @return bool true if the condition matched
*/
protected function evaluateConditionRecursive(array $conditionArray): bool
{
switch ($conditionArray['type']) {
case 'AND':
$result = true;
foreach ($conditionArray['subConditions'] as $subCondition) {
$result = $result && $this->evaluateConditionRecursive($subCondition);
}
return $result;
case 'OR':
$result = false;
foreach ($conditionArray['subConditions'] as $subCondition) {
$result = $result || $this->evaluateConditionRecursive($subCondition);
}
return $result;
case 'FIELD':
return $this->matchFieldCondition($conditionArray);
case 'HIDE_FOR_NON_ADMINS':
return (bool)$this->getBackendUser()->isAdmin();
case 'REC':
return $this->matchRecordCondition($conditionArray);
case 'VERSION':
return $this->matchVersionCondition($conditionArray);
case 'USER':
return $this->matchUserCondition($conditionArray);
}
return false;
}
/**
* Evaluates conditions concerning a field of the current record.
*
* Example:
* "FIELD:sys_language_uid:>:0" => TRUE, if the field 'sys_language_uid' is greater than 0
*
* @param array $condition Condition array
*/
protected function matchFieldCondition(array $condition): bool
{
$operator = $condition['operator'];
$operand = $condition['operand'];
$fieldValue = $condition['fieldValue'];
$result = false;
switch ($operator) {
case 'REQ':
if (is_array($fieldValue) && count($fieldValue) <= 1) {
$fieldValue = array_shift($fieldValue);
}
if ($operand) {
$result = (bool)$fieldValue;
} else {
$result = !$fieldValue;
}
break;
case '>':
if (is_array($fieldValue) && count($fieldValue) <= 1) {
$fieldValue = array_shift($fieldValue);
}
$result = $fieldValue > $operand;
break;
case '<':
if (is_array($fieldValue) && count($fieldValue) <= 1) {
$fieldValue = array_shift($fieldValue);
}
$result = $fieldValue < $operand;
break;
case '>=':
if (is_array($fieldValue) && count($fieldValue) <= 1) {
$fieldValue = array_shift($fieldValue);
}
if ($fieldValue === null) {
// If field value is null, this is NOT greater than or equal 0
// See test set "Field is not greater than or equal to zero if empty array given"
$result = false;
} else {
$result = $fieldValue >= $operand;
}
break;
case '<=':
if (is_array($fieldValue) && count($fieldValue) <= 1) {
$fieldValue = array_shift($fieldValue);
}
$result = $fieldValue <= $operand;
break;
case '-':
case '!-':
if (is_array($fieldValue) && count($fieldValue) <= 1) {
$fieldValue = array_shift($fieldValue);
}
$min = $condition['min'];
$max = $condition['max'];
$result = $fieldValue >= $min && $fieldValue <= $max;
if ($operator[0] === '!') {
$result = !$result;
}
break;
case '=':
case '!=':
if (is_array($fieldValue) && count($fieldValue) <= 1) {
$fieldValue = array_shift($fieldValue);
}
$result = $fieldValue == $operand;
if ($operator[0] === '!') {
$result = !$result;
}
break;
case 'IN':
case '!IN':
if (is_array($fieldValue)) {
$result = count(array_intersect($fieldValue, GeneralUtility::trimExplode(',', $operand))) > 0;
} else {
$result = GeneralUtility::inList($operand, $fieldValue);
}
if ($operator[0] === '!') {
$result = !$result;
}
break;
case 'BIT':
case '!BIT':
$result = (bool)((int)$fieldValue & $operand);
if ($operator[0] === '!') {
$result = !$result;
}
break;
}
return $result;
}
/**
* Evaluates conditions concerning the status of the current record.
*
* Example:
* "REC:NEW:FALSE" => TRUE, if the record is already persisted (has a uid > 0)
*
* @param array $condition Condition array
*/
protected function matchRecordCondition(array $condition): bool
{
if ($condition['isNew']) {
return !((int)$condition['uid'] > 0);
}
return (int)$condition['uid'] > 0;
}
/**
* Evaluates whether the current record is versioned.
*
* @param array $condition Condition array
*/
protected function matchVersionCondition(array $condition): bool
{
$isNewRecord = !((int)$condition['uid'] > 0);
// Detection of version can be done by detecting the workspace of the user
$isUserInWorkspace = $this->getBackendUser()->workspace > 0;
if ((int)($condition['t3ver_oid'] ?? 0) > 0) {
$isRecordDetectedAsVersion = true;
} else {
$isRecordDetectedAsVersion = false;
}
// New records in a workspace are not handled as a version record
// if it's no new version, we detect versions like this:
// * if user is in workspace: always TRUE
// * if editor is in live ws: only TRUE if t3ver_oid > 0
$result = ($isUserInWorkspace || $isRecordDetectedAsVersion) && !$isNewRecord;
if (!$condition['isVersion']) {
$result = !$result;
}
return $result;
}
/**
* Evaluates via the referenced user-defined method
*
* @param array $condition Condition array
*/
protected function matchUserCondition(array $condition): bool
{
$parameter = [
'record' => $condition['record'],
'flexContext' => $condition['flexContext'],
'flexformValueKey' => 'vDEF',
'conditionParameters' => $condition['parameters'],
];
return (bool)GeneralUtility::callUserFunction($condition['function'], $parameter, $this);
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
/**
* This class is not named properly but will be reworked in the future.
*
* Currently, it is necessary to set the TCA from the outside as it needs to be faked e.g., for edit site configuration.
* As the formengine handles inline Elements as nested call to itself the TCA and its schema needs to be hold as state.
*
* @todo: Once processedTca is refactored from its array shape remove keeping the full TCA and TCA schemata as state.
*/
readonly class InitializeProcessedTca implements FormDataProviderInterface
{
public function __construct(private TcaSchemaFactory $tcaSchemaFactory) {}
/**
* Add full TCA as copy from vanilla TCA if not already set form the outside
* Fetch TCA schemata from vanilla TCA if not already set from the outside
* Add processed TCA as copy from vanilla TCA and sanitize some details
*/
public function addData(array $result): array
{
$result = $this->initializeFullTca($result);
$result = $this->initializeTcaSchemata($result);
return $this->initializeProcessedTca($result);
}
private function initializeFullTca(array $result): array
{
if (!empty($result['fullTca'] ?? null)) {
return $result;
}
$result['fullTca'] = $GLOBALS['TCA'];
return $result;
}
private function initializeTcaSchemata(array $result): array
{
if (!empty($result['tcaSchemata'] ?? null)) {
return $result;
}
$result['tcaSchemata'] = $this->tcaSchemaFactory->all();
return $result;
}
private function initializeProcessedTca(array $result): array
{
if (empty($result['processedTca'])) {
if (
!isset($result['fullTca'][$result['tableName']])
|| !is_array($result['fullTca'][$result['tableName']])
) {
throw new \UnexpectedValueException(
'TCA for table ' . $result['tableName'] . ' not found',
1437914223
);
}
$result['processedTca'] = $result['fullTca'][$result['tableName']];
}
if (!is_array($result['processedTca']['columns'])) {
throw new \UnexpectedValueException(
'No columns definition in TCA table ' . $result['tableName'],
1438594406
);
}
return $result;
}
}
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Override child TCA in an inline parent child relation.
*
* This basically merges the inline property ['overrideChildTca'] from
* parent TCA over given child TCA.
*/
class InlineOverrideChildTca implements FormDataProviderInterface
{
/**
* ['columns'] section child TCA field names that can not be overridden
* by overrideChildTca from parent.
*
* @var array
*/
protected $notSettableFields = [
'uid',
'pid',
't3ver_oid',
't3ver_wsid',
't3ver_state',
't3ver_stage',
];
/**
* Configuration fields in ctrl section. Their values are field names and if the
* keys are set in ['ctrl'] section, they are added to the $notSettableFields list
* and can not be overridden, too.
*
* @var array
*/
protected $configurationKeysForNotSettableFields = [
'crdate',
'delete',
'origUid',
'transOrigDiffSourceField',
'transOrigPointerField',
'tstamp',
];
/**
* Inline parent TCA may override some TCA of children.
*
* @param array $result Main result array
* @return array Modified result array
*/
public function addData(array $result): array
{
$result = $this->overrideTypes($result);
return $this->overrideColumns($result);
}
/**
* Override ['types'] configuration in child TCA
*
* @param array $result Main result array
* @return array Modified result array
*/
protected function overrideTypes(array $result): array
{
if (!isset($result['inlineParentConfig']['overrideChildTca']['types'])) {
return $result;
}
$result['processedTca']['types'] = array_replace_recursive(
$result['processedTca']['types'],
$result['inlineParentConfig']['overrideChildTca']['types']
);
return $result;
}
/**
* Override ['columns'] configuration in child TCA.
* Sanitizes that various hard dependencies can not be changed.
*
* @param array $result Main result array
* @return array Modified result array
* @throws \RuntimeException
*/
protected function overrideColumns(array $result): array
{
if (!isset($result['inlineParentConfig']['overrideChildTca']['columns'])) {
return $result;
}
$fieldBlackList = $this->generateFieldBlackList($result);
foreach ($fieldBlackList as $notChangeableFieldName) {
if (isset($result['inlineParentConfig']['overrideChildTca']['columns'][$notChangeableFieldName])) {
throw new \RuntimeException(
'System field \'' . $notChangeableFieldName . '\' can not be overridden in inline config'
. ' \'overrideChildTca\' from parent TCA',
1490371322
);
}
}
$result['processedTca']['columns'] = array_replace_recursive(
$result['processedTca']['columns'],
$result['inlineParentConfig']['overrideChildTca']['columns']
);
return $result;
}
/**
* Add field names defined in ctrl section of child table to black list
*
* @param array $result Main result array
* @return array Column field names which can not be changed by parent TCA
*/
protected function generateFieldBlackList(array $result): array
{
$notSettableFields = $this->notSettableFields;
foreach ($this->configurationKeysForNotSettableFields as $configurationKey) {
if (isset($result['processedTca']['ctrl'][$configurationKey])) {
$notSettableFields[] = $result['processedTca']['ctrl'][$configurationKey];
}
}
return $notSettableFields;
}
}
@@ -0,0 +1,37 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
/**
* Page TsConfig relevant for this record
*/
class PageTsConfig implements FormDataProviderInterface
{
/**
* Add page TSconfig
*
* @return array
*/
public function addData(array $result)
{
$result['pageTsConfig'] = BackendUtility::getPagesTSconfig($result['effectivePid']);
return $result;
}
}
@@ -0,0 +1,65 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* Page TSconfig relevant for this record
*/
class PageTsConfigMerged implements FormDataProviderInterface
{
/**
* Merge type specific page TS to page TSconfig
*
* @return array
*/
public function addData(array $result)
{
$mergedTsConfig = $result['pageTsConfig'];
if (empty($result['pageTsConfig']['TCEFORM.']) || !is_array($result['pageTsConfig']['TCEFORM.'])) {
$result['pageTsConfig'] = $mergedTsConfig;
return $result;
}
$mergedTsConfig = $result['pageTsConfig'];
$type = $result['recordTypeValue'];
$table = $result['tableName'];
// Merge TCEFORM.[table name].[field].types.[type] over TCEFORM.[table name].[field]
if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'])
&& is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'])
) {
foreach ($result['pageTsConfig']['TCEFORM.'][$table . '.'] as $fieldNameWithDot => $fullFieldConfiguration) {
$newFieldConfiguration = $fullFieldConfiguration;
if (!empty($fullFieldConfiguration['types.']) && is_array($fullFieldConfiguration['types.'])) {
$typeSpecificConfiguration = $newFieldConfiguration['types.'];
unset($newFieldConfiguration['types.']);
if (!empty($typeSpecificConfiguration[$type . '.']) && is_array($typeSpecificConfiguration[$type . '.'])) {
ArrayUtility::mergeRecursiveWithOverrule($newFieldConfiguration, $typeSpecificConfiguration[$type . '.']);
}
}
$mergedTsConfig['TCEFORM.'][$table . '.'][$fieldNameWithDot] = $newFieldConfiguration;
}
}
$result['pageTsConfig'] = $mergedTsConfig;
return $result;
}
}
@@ -0,0 +1,65 @@
<?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 Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Routing\UriBuilder;
/**
* Resolve return Url if not set from outside. This is used
* by various field elements when a "sub" FormEngine view
* is triggered. An example is the "Add" button on type="group"
* elements.
*
* @todo: We may want to get rid of this eventually: The returnUrl
* should typically be set by calling controllers as initial
* data, since only controllers know details about current
* context. The fallback below is a bit of guesswork.
*/
readonly class ReturnUrl implements FormDataProviderInterface
{
public function __construct(
private UriBuilder $uriBuilder,
) {}
public function addData(array $result): array
{
if ($result['returnUrl'] !== null) {
return $result;
}
/** @var ServerRequestInterface $request */
$request = $result['request'];
$routeIdentifier = $request->getAttribute('route')?->getOption('_identifier');
if ($routeIdentifier === null) {
// Route could not be found. This usually should not happen in any
// backend context, but is sanitized here nevertheless. returnUrl
// will be kept as null in this case, which may or may not trigger
// subsequent issues.
return $result;
}
$queryParams = $request->getQueryParams();
$relevantQueryParams = [];
foreach ($queryParams as $queryKey => $queryValue) {
if (in_array($queryKey, ['token', 'returnUrl'], true)) {
continue;
}
$relevantQueryParams[$queryKey] = $queryValue;
}
$result['returnUrl'] = (string)$this->uriBuilder->buildUriFromRoute($routeIdentifier, $relevantQueryParams);
return $result;
}
}
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Configuration\Processor\Placeholder\EnvPlaceholderProcessor;
use TYPO3\CMS\Core\Configuration\SiteConfiguration;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Special data provider for the sites configuration module.
*
* Fetch "row" data from yml file and set as 'databaseRow'
*/
readonly class SiteDatabaseEditRow implements FormDataProviderInterface
{
public function __construct(
private SiteFinder $siteFinder,
private SiteTcaConfiguration $siteTcaConfiguration,
private EnvPlaceholderProcessor $envPlaceholderProcessor,
) {}
/**
* First level of ['customData']['siteData'] to ['databaseRow']
*
* @throws \RuntimeException
*/
public function addData(array $result): array
{
if ($result['command'] !== 'edit' || !empty($result['databaseRow'])) {
return $result;
}
$tableName = $result['tableName'];
if ($tableName === 'site') {
$rootPageId = (int)$result['vanillaUid'];
$rowData = $this->getRawConfigurationForSiteWithRootPageId($rootPageId);
$result['databaseRow']['uid'] = $rowData['rootPageId'];
$result['databaseRow']['identifier'] = $result['customData']['siteIdentifier'];
} elseif (in_array($tableName, ['site_errorhandling', 'site_language', 'site_route', 'site_base_variant'], true)) {
$unprocessedRootPageId = $result['inlineTopMostParentUid'] ?? $result['inlineParentUid'];
$processedRootPageId = $this->envPlaceholderProcessor->canProcess($unprocessedRootPageId)
? (int)$this->envPlaceholderProcessor->process($unprocessedRootPageId)
: (int)$unprocessedRootPageId;
try {
$rowData = $this->getRawConfigurationForSiteWithRootPageId($processedRootPageId);
$parentFieldName = $result['inlineParentFieldName'];
if (!isset($rowData[$parentFieldName])) {
throw new \RuntimeException('Field "' . $parentFieldName . '" not found', 1520886092);
}
$rowData = $rowData[$parentFieldName][$result['vanillaUid']];
$result['databaseRow']['uid'] = $result['vanillaUid'];
} catch (SiteNotFoundException $e) {
$rowData = [];
}
} else {
throw new \RuntimeException('Other tables not implemented', 1520886234);
}
foreach ($rowData as $fieldName => $value) {
// Flat values only - databaseRow has no "tree"
if (!is_array($value)) {
$result['databaseRow'][$fieldName] = $value;
}
}
// All "records" are always on pid 0
$result['databaseRow']['pid'] = 0;
return $result;
}
protected function getRawConfigurationForSiteWithRootPageId(int $rootPageId): array
{
$site = $this->siteFinder->getSiteByRootPageId($rootPageId);
// load config as it is stored on disk (without replacements)
$siteTca = $this->siteTcaConfiguration->getTca();
$configuration = GeneralUtility::makeInstance(SiteConfiguration::class)->load($site->getIdentifier());
foreach ($configuration as $fieldName => $fieldValue) {
if (is_array($fieldValue) && ($siteTca['site']['columns'][$fieldName]['config']['type'] ?? '') === 'select' && ($siteTca['site']['columns'][$fieldName]['config']['renderType'] ?? '') === 'selectMultipleSideBySide') {
$configuration[$fieldName] = implode(',', $fieldValue);
}
}
return $configuration;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Site\Entity\NullSite;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* This data provider is used in casual edit record / new record / edit page / new page
* scenarios: It find the site object for a page and adds it as 'site' in $result.
*
* Note this data provider has a loose dependency to DatabaseDefaultLanguagePageRow,
* it needs that to determine the correct base pid if localized pages are edited.
*/
readonly class SiteResolving implements FormDataProviderInterface
{
public function __construct(
private SiteFinder $siteFinder,
) {}
/**
* Find and add site object
*/
public function addData(array $result): array
{
if ($result['defaultLanguagePageRow']['t3ver_oid'] ?? null) {
$pageIdDefaultLanguage = (int)$result['defaultLanguagePageRow']['t3ver_oid'];
} elseif ($result['defaultLanguagePageRow']['uid'] ?? null) {
$pageIdDefaultLanguage = (int)$result['defaultLanguagePageRow']['uid'];
} elseif (array_key_exists('tableName', $result) && $result['tableName'] === 'pages') {
if (!empty($result['databaseRow']['t3ver_oid'])) {
$pageIdDefaultLanguage = $result['databaseRow']['t3ver_oid'];
} elseif (MathUtility::canBeInterpretedAsInteger($result['databaseRow']['uid'] ?? '')) {
$pageIdDefaultLanguage = $result['databaseRow']['uid'];
} else {
$pageIdDefaultLanguage = $result['effectivePid'];
}
} else {
$pageIdDefaultLanguage = $result['effectivePid'];
}
$result['site'] = $this->resolveSite((int)$pageIdDefaultLanguage);
return $result;
}
protected function resolveSite(int $pageId): SiteInterface
{
try {
return $this->siteFinder->getSiteByPageId($pageId);
} catch (SiteNotFoundException $e) {
return new NullSite();
}
}
}
@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Special data provider for the sites configuration module.
*
* Handle inline children of 'site'
*/
class SiteTcaInline extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
public function __construct(
private readonly SiteFinder $siteFinder,
private readonly InlineStackProcessor $inlineStackProcessor,
) {}
/**
* Resolve inline fields
*/
public function addData(array $result): array
{
$result = $this->addInlineFirstPid($result);
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (!$this->isInlineField($fieldConfig)) {
continue;
}
$childTableName = $fieldConfig['config']['foreign_table'] ?? '';
if (!in_array($childTableName, ['site_errorhandling', 'site_route', 'site_base_variant'], true)) {
throw new \RuntimeException('Inline relation to other tables not implemented', 1522494737);
}
$result['processedTca']['columns'][$fieldName]['children'] = [];
$result = $this->resolveSiteRelatedChildren($result, $fieldName);
if (!empty($result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'])) {
throw new \RuntimeException('selectorOrUniqueConfiguration not implemented in sites module', 1624313533);
}
}
return $result;
}
/**
* Is column of type "inline"
*/
protected function isInlineField(array $fieldConfig): bool
{
return !empty($fieldConfig['config']['type']) && $fieldConfig['config']['type'] === 'inline';
}
/**
* The "entry" pid for inline records. Nested inline records can potentially hang around on different
* pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes place on the page structure.
*
* @param array $result Incoming result
* @return array Modified result
* @todo: Find out when and if this is different from 'effectivePid'
*/
protected function addInlineFirstPid(array $result): array
{
if ($result['inlineFirstPid'] === null) {
$table = $result['tableName'];
$row = $result['databaseRow'];
// If the parent is a page, use the uid(!) of the (new?) page as pid for the child records:
if ($table === 'pages') {
$liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']);
$pid = $liveVersionId ?? $row['uid'];
} elseif (($row['pid'] ?? 0) < 0) {
$prevRec = BackendUtility::getRecord($table, (int)abs($row['pid']));
$pid = $prevRec['pid'];
} else {
$pid = $row['pid'] ?? 0;
}
if (MathUtility::canBeInterpretedAsInteger($pid)) {
$pageRecord = BackendUtility::getRecord('pages', (int)$pid);
$pageSchema = $result['tcaSchemata']->has('pages') ? $result['tcaSchemata']->get('pages') : null;
if ($pageSchema !== null
&& $pageSchema->hasCapability(TcaSchemaCapability::Language)
&& ($pageRecord[$pageSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()] ?? 0) > 0) {
$pid = (int)$pageRecord[$pageSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()];
}
} elseif (!str_starts_with($pid, 'NEW')) {
throw new \RuntimeException(
'inlineFirstPid should either be an integer or a "NEW..." string',
1521220141
);
}
$result['inlineFirstPid'] = $pid;
}
return $result;
}
/**
* Substitute the value in databaseRow of this inline field with an array
* that contains the databaseRows of currently connected records and some meta information.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
*/
protected function resolveSiteRelatedChildren(array $result, string $fieldName): array
{
$connectedUids = [];
if ($result['command'] === 'edit') {
$siteConfigurationForPageUid = (int)$result['databaseRow']['rootPageId'][0];
try {
$site = $this->siteFinder->getSiteByRootPageId($siteConfigurationForPageUid);
} catch (SiteNotFoundException $e) {
$site = null;
}
$siteConfiguration = $site ? $site->getConfiguration() : [];
if (is_array($siteConfiguration[$fieldName] ?? false)) {
$connectedUids = array_keys($siteConfiguration[$fieldName]);
}
}
$result['databaseRow'][$fieldName] = implode(',', $connectedUids);
if ($result['inlineCompileExistingChildren']) {
foreach ($connectedUids as $uid) {
if (!str_starts_with((string)$uid, 'NEW')) {
$compiledChild = $this->compileChild($result, $fieldName, (int)$uid);
$result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
}
}
}
return $result;
}
/**
* Compile a full child record
*
* @param array $result Result array of parent
* @param string $parentFieldName Name of parent field
* @param int $childUid Uid of child to compile
* @return array Full result array
*/
protected function compileChild(array $result, string $parentFieldName, int $childUid): array
{
$parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
$childTableName = $parentConfig['foreign_table'];
$inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($result['inlineStructure'], 0);
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class);
$formDataCompilerInput = [
'request' => $result['request'],
'command' => 'edit',
'tableName' => $childTableName,
'vanillaUid' => $childUid,
// Give incoming returnUrl down to children so they generate a returnUrl back to
// the originally opening record, also see "originalReturnUrl" in inline container
// and FormInlineAjaxController
'returnUrl' => $result['returnUrl'],
'isInlineChild' => true,
'inlineStructure' => $result['inlineStructure'],
'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
'inlineFirstPid' => $result['inlineFirstPid'],
'inlineParentConfig' => $parentConfig,
// values of the current parent element
// it is always a string either an id or new...
'inlineParentUid' => $result['databaseRow']['uid'],
'inlineParentTableName' => $result['tableName'],
'inlineParentFieldName' => $parentFieldName,
// values of the top most parent element set on first level and not overridden on following levels
'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: ($inlineTopMostParent['uid'] ?? null),
'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: ($inlineTopMostParent['table'] ?? ''),
'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: ($inlineTopMostParent['field'] ?? ''),
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
];
if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) {
throw new \RuntimeException('useCombination not implemented in sites module', 1522493097);
}
return $formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(SiteConfigurationDataGroup::class));
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
/**
* Special data provider for setting all fields of the current
* record to "readOnly" in case a non system maintainer is editing
* a system maintainer record.
*/
readonly class SystemMaintainerAsReadonly implements FormDataProviderInterface
{
public function __construct(
private FlashMessageService $flashMessageService,
) {}
public function addData(array $result): array
{
if ($result['tableName'] !== 'be_users' || $result['command'] !== 'edit') {
return $result;
}
$id = (int)$result['vanillaUid'];
$systemMaintainers = array_map(intval(...), $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
$isCurrentUserSystemMaintainer = $this->getBackendUser()->isSystemMaintainer();
$isTargetUserInSystemMaintainerList = in_array($id, $systemMaintainers, true);
if (!$isCurrentUserSystemMaintainer && $isTargetUserInSystemMaintainerList) {
$message = $this->getLanguageService()->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:formEngine.beUser.information.adminCanNotChangeSystemMaintainer'
);
$flashMessage = new FlashMessage(
$message,
'',
ContextualFeedbackSeverity::INFO
);
$this->flashMessageService->getMessageQueueByIdentifier()->enqueue($flashMessage);
foreach ($result['processedTca']['columns'] as &$fieldConfig) {
$fieldConfig['config']['readOnly'] = true;
}
}
return $result;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Tree\TableConfiguration\ArrayTreeRenderer;
use TYPO3\CMS\Core\Tree\TableConfiguration\TableConfigurationTree;
use TYPO3\CMS\Core\Tree\TableConfiguration\TreeDataProviderFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Data provider for type=category
*
* Used in combination with CategoryElement to create the base HTML for the category tree.
*
* Used in combination with FormSelectTreeAjaxController to fetch the final tree list, this
* is triggered if $result['selectTreeCompileItems'] is set to true. This way the tree item
* calculation is only triggered if needed in this ajax context. Writes the prepared item
* array to ['config']['items'] in this case.
*/
class TcaCategory extends AbstractItemProvider implements FormDataProviderInterface
{
/**
* Sanitize config options and resolve category items if requested.
*/
public function addData(array $result): array
{
$table = $result['tableName'];
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
// This data provider only works for type=category
if (($fieldConfig['config']['type'] ?? '') !== 'category') {
continue;
}
// Make sure we are only processing supported renderTypes
if (!$this->isTargetRenderType($fieldConfig)) {
continue;
}
$fieldConfig = $this->initializeDefaultFieldConfig($fieldConfig);
$fieldConfig = $this->parseStartingPointsFromSiteConfiguration($result, $fieldConfig);
$fieldConfig = $this->overrideConfigFromPageTSconfig($result, $table, $fieldName, $fieldConfig);
// Prepare the list of currently selected nodes using RelationHandler
// This is needed to ensure a correct value initialization before the actual tree is loaded
$result['databaseRow'][$fieldName] = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName);
$result['databaseRow'][$fieldName] = $this->processCategoryFieldValue($result, $fieldName);
// Since AbstractItemProvider does sometimes access $result[...][config] instead of
// our updated $fieldConfig, we have to assign it here and from now on, only work
// with the $result[...][config] array.
$result['processedTca']['columns'][$fieldName] = $fieldConfig;
// Validate: static items are not supported for category fields
$staticItems = $this->sanitizeItemArray($fieldConfig['config']['items'] ?? [], $table, $fieldName);
$tsConfigItems = $this->addItemsFromPageTsConfig($result, $fieldName, []);
if ($staticItems !== [] || $tsConfigItems !== []) {
throw new \RuntimeException(
'Static items are not supported for field ' . $fieldName . ' from table ' . $table . ' with type category',
1627336557
);
}
// Always resolve the flat item list from foreign_table with TSconfig filtering applied.
// This is needed by TcaColumnsRemoveEmptyRelations to determine if the field has any
// selectable items, and is reused below for tree building in the AJAX context.
$dynamicItems = $this->addItemsFromForeignTable($result, $fieldName);
$dynamicItems = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $dynamicItems);
$dynamicItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $dynamicItems);
// Store flat items for downstream providers (will be overwritten with tree structure during AJAX)
$result['processedTca']['columns'][$fieldName]['config']['items'] = $dynamicItems;
// This is usually only executed in an ajax request
if ($result['selectTreeCompileItems'] ?? false) {
// Reuse the already-fetched dynamic items for tree building
// @todo: Simplify the construct:
// The entire $treeDataProvider / $treeRenderer / $tree construct should probably
// vanish and the tree processing could happen here in the data provider? Watch
// out for the permission event in the tree construct when doing this.
$uidListOfAllDynamicItems = array_map(intval(...), array_filter(
array_column($dynamicItems, 'value'),
static fn($uid) => (int)$uid > 0
));
$fullRowsOfDynamicItems = [];
foreach ($dynamicItems as $item) {
// @todo: Prepare performance hack for tree calculation below.
if (isset($item['_row'])) {
$fullRowsOfDynamicItems[(int)$item['_row']['uid']] = $item['_row'];
}
}
// Initialize the tree data provider
$treeDataProvider = TreeDataProviderFactory::getDataProvider(
$result['processedTca']['columns'][$fieldName]['config'],
$table,
$fieldName,
$result['databaseRow']
);
$treeDataProvider->setSelectedList(implode(',', $result['databaseRow'][$fieldName]));
// Basically the tree data provider fetches all tree nodes again and
// then verifies if a given rows' uid is within the item whitelist.
// @todo: Simplify construct, probably remove entirely. See @todo above as well.
$treeDataProvider->setAvailableItems($fullRowsOfDynamicItems);
$treeDataProvider->setItemWhiteList($uidListOfAllDynamicItems);
$treeDataProvider->initializeTreeData();
$treeRenderer = GeneralUtility::makeInstance(ArrayTreeRenderer::class);
$tree = GeneralUtility::makeInstance(TableConfigurationTree::class);
$tree->setDataProvider($treeDataProvider);
$tree->setNodeRenderer($treeRenderer);
// Add the calculated tree nodes
$result['processedTca']['columns'][$fieldName]['config']['items'] = $tree->render();
}
}
return $result;
}
/**
* A couple of tree specific config parameters can be overwritten via page TS.
* Pick those that influence the data fetching and write them into the config
* given to the tree data provider.
*/
protected function overrideConfigFromPageTSconfig(
array $result,
string $table,
string $fieldName,
array $fieldConfig
): array {
$pageTsConfig = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['config.']['treeConfig.'] ?? [];
if (!is_array($pageTsConfig) || $pageTsConfig === []) {
return $fieldConfig;
}
if (isset($pageTsConfig['startingPoints'])) {
$fieldConfig['config']['treeConfig']['startingPoints'] = implode(',', array_unique(GeneralUtility::intExplode(',', (string)$pageTsConfig['startingPoints'])));
}
if (isset($pageTsConfig['appearance.']['expandAll'])) {
$fieldConfig['config']['treeConfig']['appearance']['expandAll'] = (bool)$pageTsConfig['appearance.']['expandAll'];
}
if (isset($pageTsConfig['appearance.']['maxLevels'])) {
$fieldConfig['config']['treeConfig']['appearance']['maxLevels'] = (int)$pageTsConfig['appearance.']['maxLevels'];
}
if (isset($pageTsConfig['appearance.']['nonSelectableLevels'])) {
$fieldConfig['config']['treeConfig']['appearance']['nonSelectableLevels'] = $pageTsConfig['appearance.']['nonSelectableLevels'];
}
return $fieldConfig;
}
/**
* Validate and sanitize the category field value.
*/
protected function processCategoryFieldValue(array $result, string $fieldName): array
{
$fieldConfig = $result['processedTca']['columns'][$fieldName];
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$newDatabaseValueArray = [];
$currentDatabaseValueArray = array_key_exists($fieldName, $result['databaseRow']) ? $result['databaseRow'][$fieldName] : [];
$relationHandler->initializeForField(
$result['tableName'],
$fieldConfig['config'],
$result['databaseRow'],
implode(',', $currentDatabaseValueArray),
);
$newDatabaseValueArray = array_merge($newDatabaseValueArray, $relationHandler->getValueArray());
if (empty($fieldConfig['config']['MM']) || $result['command'] === 'new') {
// remove all items from the current DB values if not available as relation
$newDatabaseValueArray = array_values(array_intersect($currentDatabaseValueArray, $newDatabaseValueArray));
}
// Since only uids are allowed, the array must be unique
return array_unique($newDatabaseValueArray);
}
protected function isTargetRenderType($fieldConfig): bool
{
// Type category does not support any renderType
return !isset($fieldConfig['config']['renderType']);
}
protected function initializeDefaultFieldConfig(array $fieldConfig): array
{
$fieldConfig = array_replace_recursive([
'config' => [
'treeConfig' => [
'parentField' => 'parent',
'appearance' => [
'expandAll' => true,
'showHeader' => true,
'maxLevels' => 99,
],
],
],
], $fieldConfig);
// Calculate maxitems value, while 0 will fall back to 99999
$fieldConfig['config']['maxitems'] = MathUtility::forceIntegerInRange(
$fieldConfig['config']['maxitems'] ?? 0,
0,
99999
) ?: 99999;
return $fieldConfig;
}
}
@@ -0,0 +1,168 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Resolve checkbox items and set processed item list in processedTca
*/
class TcaCheckboxItems extends AbstractItemProvider implements FormDataProviderInterface
{
/**
* Resolve checkbox items
*
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
$languageService = $this->getLanguageService();
$table = $result['tableName'];
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'check') {
continue;
}
if (!is_array($fieldConfig['config']['items'] ?? null)) {
$fieldConfig['config']['items'] = [];
}
$config = $fieldConfig['config'];
$items = $this->sanitizeConfiguration($config, $fieldName, $table);
// Resolve "itemsProcFunc"
if (!empty($config['itemsProcFunc']) || !empty($config['itemsProcessors'])) {
$items = $this->resolveItemsProcessorFunction($result, $fieldName, $items);
// itemsProcFunc must not be used anymore
unset(
$result['processedTca']['columns'][$fieldName]['config']['itemsProcFunc'],
$result['processedTca']['columns'][$fieldName]['config']['itemsProcessors']
);
}
// Set label overrides from pageTsConfig if given
if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'])
&& is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'])
) {
foreach ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'] as $itemKey => $label) {
if (isset($items[$itemKey]['label'])) {
$items[$itemKey]['label'] = $languageService->sL($label);
}
}
}
$result['processedTca']['columns'][$fieldName]['config']['items'] = $items;
}
return $result;
}
/**
* @return array
* @throws \UnexpectedValueException
*/
private function sanitizeConfiguration(array $config, string $fieldName, string $tableName)
{
$newItems = [];
foreach ($config['items'] as $itemKey => $checkboxEntry) {
$this->basicChecks($fieldName, $tableName, $checkboxEntry, $itemKey);
$newItems[$itemKey] = [
'label' => $this->getLanguageService()->sL(trim($checkboxEntry['label'])),
];
if (isset($config['renderType']) && $config['renderType'] === 'checkboxToggle') {
$newItems = $this->sanitizeToggleCheckbox($checkboxEntry, $itemKey, $newItems);
} elseif (isset($config['renderType']) && $config['renderType'] === 'checkboxLabeledToggle') {
$newItems = $this->sanitizeLabeledToggleCheckbox($checkboxEntry, $itemKey, $newItems);
} else {
$newItems = $this->sanitizeIconToggleCheckbox($checkboxEntry, $itemKey, $newItems);
}
}
return $newItems;
}
/**
* @param mixed $checkboxEntry
* @throws \UnexpectedValueException
*/
private function basicChecks(string $fieldName, string $tableName, $checkboxEntry, int $checkboxKey)
{
if (!is_array($checkboxEntry)) {
throw new \UnexpectedValueException(
'Item ' . $checkboxKey . ' of field ' . $fieldName . ' of TCA table ' . $tableName . ' is not an array as expected',
1440499337
);
}
if (!array_key_exists('label', $checkboxEntry)) {
throw new \UnexpectedValueException(
'Item ' . $checkboxKey . ' of field ' . $fieldName . ' of TCA table ' . $tableName . ' has no label',
1440499338
);
}
}
/**
* @return array
*/
private function sanitizeToggleCheckbox(array $item, int $itemKey, array $newItems)
{
if (array_key_exists('invertStateDisplay', $item)) {
$newItems[$itemKey]['invertStateDisplay'] = (bool)$item['invertStateDisplay'];
} else {
$newItems[$itemKey]['invertStateDisplay'] = false;
}
return $newItems;
}
/**
* @return array
*/
private function sanitizeLabeledToggleCheckbox(array $item, int $itemKey, array $newItems)
{
if (array_key_exists('labelChecked', $item)) {
$newItems[$itemKey]['labelChecked'] = $this->getLanguageService()->sL($item['labelChecked']);
}
if (array_key_exists('labelUnchecked', $item)) {
$newItems[$itemKey]['labelUnchecked'] = $this->getLanguageService()->sL($item['labelUnchecked']);
}
if (array_key_exists('invertStateDisplay', $item)) {
$newItems[$itemKey]['invertStateDisplay'] = (bool)$item['invertStateDisplay'];
} else {
$newItems[$itemKey]['invertStateDisplay'] = false;
}
return $newItems;
}
/**
* @return array
*/
private function sanitizeIconToggleCheckbox(array $item, int $itemKey, array $newItems)
{
if (array_key_exists('iconIdentifierChecked', $item)) {
$newItems[$itemKey]['iconIdentifierChecked'] = $item['iconIdentifierChecked'];
}
if (array_key_exists('iconIdentifierUnchecked', $item)) {
$newItems[$itemKey]['iconIdentifierUnchecked'] = $item['iconIdentifierUnchecked'];
}
if (array_key_exists('invertStateDisplay', $item)) {
$newItems[$itemKey]['invertStateDisplay'] = (bool)$item['invertStateDisplay'];
} else {
$newItems[$itemKey]['invertStateDisplay'] = false;
}
return $newItems;
}
}
@@ -0,0 +1,58 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Merge type specific columnsOverrides into columns of processedTca
*/
class TcaColumnsOverrides implements FormDataProviderInterface
{
/**
* Merge columnsOverrides
*
* @return array
*/
public function addData(array $result)
{
$type = $result['recordTypeValue'];
if (isset($result['processedTca']['types'][$type]['columnsOverrides'])
&& is_array($result['processedTca']['types'][$type]['columnsOverrides'])
) {
$result['processedTca']['columns'] = array_replace_recursive(
$result['processedTca']['columns'],
$result['processedTca']['types'][$type]['columnsOverrides']
);
if ($result['command'] === 'new') {
$tableNameWithDot = $result['tableName'] . '.';
foreach ($result['processedTca']['types'][$type]['columnsOverrides'] as $field => $columnsOverrideConfig) {
$overridenDefault = $columnsOverrideConfig['config']['default'] ?? '';
if ($overridenDefault !== ''
&& !isset($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot][$field])
&& !isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot][$field])
&& !isset($result['defaultValues'][$result['tableName']][$field])
&& ($result['databaseRow'][$field] ?? '') !== $overridenDefault
) {
$result['databaseRow'][$field] = $overridenDefault;
}
}
}
unset($result['processedTca']['types'][$type]['columnsOverrides']);
}
return $result;
}
}
@@ -0,0 +1,66 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Mark columns that are common to many tables for further processing
*/
class TcaColumnsProcessCommon implements FormDataProviderInterface
{
/**
* Determine which common fields are in use and add those to the list of
* columns that must be processed by the next data providers. Common fields
* are for example uid, transOrigPointerField or transOrigDiffSourceField.
*
*
* @return array
*/
public function addData(array $result)
{
// enables the backend to display a visual comparison between a new version and its original
$tableProperties = $result['processedTca']['ctrl'];
if (!empty($tableProperties['origUid'])) {
$result['columnsToProcess'][] = $tableProperties['origUid'];
}
// determines which one of the 'types' configurations are used for displaying the fields in the backend
if (!empty($tableProperties['type'])) {
// Allow for relation_field:foreign_type_field syntax
$fieldName = GeneralUtility::trimExplode(':', $tableProperties['type'], true, 2);
$result['columnsToProcess'][] = $fieldName[0];
}
// field that contains the language of the record
if (!empty($tableProperties['languageField'])) {
$result['columnsToProcess'][] = $tableProperties['languageField'];
}
// field that contains the pointer to the original record
if (!empty($tableProperties['transOrigPointerField'])) {
$result['columnsToProcess'][] = $tableProperties['transOrigPointerField'];
}
// field that contains the value of the original language record
if (!empty($tableProperties['transOrigDiffSourceField'])) {
$result['columnsToProcess'][] = $tableProperties['transOrigDiffSourceField'];
}
return $result;
}
}
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* Works on processedTca to determine the final value of field descriptions.
*
* processedTca['columns']['aField']['description']
*/
class TcaColumnsProcessFieldDescriptions implements FormDataProviderInterface
{
/**
* Iterate over all processedTca columns fields
*
* @param array $result Result array
* @return array Modified result array
*/
public function addData(array $result): array
{
$result = $this->setDescriptionFromPageTsConfig($result);
$result = $this->translateDescriptions($result);
return $result;
}
/**
* page TSconfig can override description:
*
* TCEFORM.aTable.aField.description = override
* TCEFORM.aTable.aField.description.en = override
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setDescriptionFromPageTsConfig(array $result): array
{
$languageService = $this->getLanguageService();
$tableName = $result['tableName'];
foreach ($result['processedTca']['columns'] ?? [] as $fieldName => $fieldConfiguration) {
$fieldTSconfig = $result['pageTsConfig']['TCEFORM.'][$tableName . '.'][$fieldName . '.'] ?? null;
if (!is_array($fieldTSconfig)) {
continue;
}
$pageTsConfigDescription = $languageService->translateLabel(
$fieldTSconfig['description.'] ?? [],
$fieldTSconfig['description'] ?? ''
);
if ($pageTsConfigDescription !== '') {
$result['processedTca']['columns'][$fieldName]['description'] = $pageTsConfigDescription;
}
}
return $result;
}
/**
* Translate all descriptions if needed.
*
* @param array $result Result array
* @return array Modified result array
*/
protected function translateDescriptions(array $result): array
{
$languageService = $this->getLanguageService();
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfiguration) {
if (!isset($fieldConfiguration['description'])) {
continue;
}
$result['processedTca']['columns'][$fieldName]['description'] = $languageService->sL($fieldConfiguration['description']);
}
return $result;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,162 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Works on processedTca to determine the final value of field labels.
*
* processedTca['columns]['aField']['label']
*/
class TcaColumnsProcessFieldLabels implements FormDataProviderInterface
{
/**
* Iterate over all processedTca columns fields
*
* @param array $result Result array
* @return array Modified result array
*/
public function addData(array $result)
{
$result = $this->setLabelFromShowitemAndPalettes($result);
$result = $this->setLabelFromPageTsConfig($result);
$result = $this->translateLabels($result);
return $result;
}
/**
* The label of a single field can be set in the showitem configuration
* of the record type and as palettes showitem as second ";" separated argument:
*
* processedTca['types']['aType']['showitem'] = 'aFieldName;aLabelOverride, --palette--;;aPaletteName'
* processedTca['palettes']['aPaletteName']['showitem'] = 'anotherFieldName;anotherLabelOverride'
*
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setLabelFromShowitemAndPalettes(array $result)
{
$recordTypeValue = $result['recordTypeValue'];
// flex forms don't have a showitem / palettes configuration - early return
if (!isset($result['processedTca']['types'][$recordTypeValue]['showitem'])) {
return $result;
}
$showItemArray = GeneralUtility::trimExplode(',', $result['processedTca']['types'][$recordTypeValue]['showitem']);
foreach ($showItemArray as $aShowItemFieldString) {
$aShowItemFieldArray = GeneralUtility::trimExplode(';', $aShowItemFieldString);
$aShowItemFieldArray = [
'fieldName' => $aShowItemFieldArray[0],
'fieldLabel' => !empty($aShowItemFieldArray[1]) ? $aShowItemFieldArray[1] : null,
'paletteName' => !empty($aShowItemFieldArray[2]) ? $aShowItemFieldArray[2] : null,
];
if ($aShowItemFieldArray['fieldName'] === '--div--') {
// tabs are not of interest here
continue;
}
if ($aShowItemFieldArray['fieldName'] === '--palette--') {
// showitem references to a palette field. unpack the palette and process
// label overrides that may be in there.
if (!isset($result['processedTca']['palettes'][$aShowItemFieldArray['paletteName'] ?? '']['showitem'])) {
// No palette with this name found? Skip it.
continue;
}
$palettesArray = GeneralUtility::trimExplode(
',',
$result['processedTca']['palettes'][$aShowItemFieldArray['paletteName']]['showitem']
);
foreach ($palettesArray as $aPalettesString) {
$aPalettesArray = GeneralUtility::trimExplode(';', $aPalettesString);
$aPalettesArray = [
'fieldName' => $aPalettesArray[0],
'fieldLabel' => ($aPalettesArray[1] ?? null) ?: null,
];
if (!empty($aPalettesArray['fieldLabel'])
&& isset($result['processedTca']['columns'][$aPalettesArray['fieldName']])
) {
$result['processedTca']['columns'][$aPalettesArray['fieldName']]['label'] = $aPalettesArray['fieldLabel'];
}
}
} else {
// If the field has a label in the showitem configuration of this record type, use it.
// showitem = 'aField, aFieldWithLabelOverride;theLabel, anotherField'
if (!empty($aShowItemFieldArray['fieldLabel'])
&& isset($result['processedTca']['columns'][$aShowItemFieldArray['fieldName']])
) {
$result['processedTca']['columns'][$aShowItemFieldArray['fieldName']]['label'] = $aShowItemFieldArray['fieldLabel'];
}
}
}
return $result;
}
/**
* Page TSconfig can override labels:
*
* TCEFORM.aTable.aField.label = 'override'
* TCEFORM.aTable.aField.label.en = 'override'
*
* @param array $result Result array
* @return array Modified result array
*/
protected function setLabelFromPageTsConfig(array $result)
{
$languageService = $this->getLanguageService();
$table = $result['tableName'];
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfiguration) {
$fieldTSConfig = [];
if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
&& is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
) {
$fieldTSConfig = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'];
}
$label = $languageService->translateLabel(
$fieldTSConfig['label.'] ?? [],
$fieldTSConfig['label'] ?? ''
);
if ($label) {
$result['processedTca']['columns'][$fieldName]['label'] = $label;
}
}
return $result;
}
/**
* Translate all labels if needed.
*
* @param array $result Result array
* @return array Modified result array
*/
protected function translateLabels(array $result)
{
$languageService = $this->getLanguageService();
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfiguration) {
if (!isset($fieldConfiguration['label'])) {
continue;
}
$result['processedTca']['columns'][$fieldName]['label'] = $languageService->sL($fieldConfiguration['label']);
}
return $result;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,73 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Mark columns that are used by placeholders for further processing
*/
class TcaColumnsProcessPlaceholders implements FormDataProviderInterface
{
/**
* Determine which fields are required to render the placeholders and
* add those to the list of columns that must be processed by the next
* data providers.
*
* @return array
*/
public function addData(array $result)
{
/** @noinspection PhpUnusedLocalVariableInspection leave for debugging purpose */
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
// Placeholders are only valid for input-like and text-like fields.
if (!isset($fieldConfig['config']['placeholder'], $fieldConfig['config']['type'])
|| (
$fieldConfig['config']['type'] !== 'input'
&& $fieldConfig['config']['type'] !== 'text'
&& $fieldConfig['config']['type'] !== 'number'
&& $fieldConfig['config']['type'] !== 'email'
&& $fieldConfig['config']['type'] !== 'link'
&& $fieldConfig['config']['type'] !== 'password'
&& $fieldConfig['config']['type'] !== 'datetime'
&& $fieldConfig['config']['type'] !== 'color'
&& $fieldConfig['config']['type'] !== 'json'
)
) {
continue;
}
// Process __row|field type placeholders
if (str_starts_with($fieldConfig['config']['placeholder'], '__row|')) {
// split field names into array and remove the __row indicator
$fieldNameArray = array_slice(
GeneralUtility::trimExplode('|', $fieldConfig['config']['placeholder'], true),
1
);
// only the first field is required to be processed as it's the one referring to
// the current record. All other columns will be resolved in a later pass through
// the related records.
if (!empty($fieldNameArray[0])) {
$result['columnsToProcess'][] = $fieldNameArray[0];
}
}
}
return $result;
}
}
@@ -0,0 +1,65 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Mark columns that are used to generate the record title for
* further processing
*/
class TcaColumnsProcessRecordTitle implements FormDataProviderInterface
{
/**
* Determine which fields are required to render the record title and
* add those to the list of columns that must be processed by the next
* data providers.
*
* @return array
*/
public function addData(array $result)
{
// formattedLabel_userFunc takes full precedence for inline children: no field
// processing is needed since the user function receives the full databaseRow directly.
if ($result['isInlineChild'] && !empty($result['processedTca']['ctrl']['formattedLabel_userFunc'])) {
return $result;
}
// If a field name is given for the label we need to process the field
if (!empty($result['processedTca']['ctrl']['label'])) {
$result['columnsToProcess'][] = $result['processedTca']['ctrl']['label'];
}
// Add alternative fields that might be used to render the label
if (!empty($result['processedTca']['ctrl']['label_alt'])) {
$labelColumns = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['label_alt'], true);
$result['columnsToProcess'] = array_merge($result['columnsToProcess'], array_filter($labelColumns));
}
// Add foreign_label to process list if exists and the record is an inline child
if ($result['isInlineChild'] && isset($result['inlineParentConfig']['foreign_label'])) {
$result['columnsToProcess'][] = $result['inlineParentConfig']['foreign_label'];
}
// Add symmetric_label to process list if exists and the record is an inline child
if ($result['isInlineChild'] && isset($result['inlineParentConfig']['symmetric_label'])) {
$result['columnsToProcess'][] = $result['inlineParentConfig']['symmetric_label'];
}
return $result;
}
}
@@ -0,0 +1,82 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Mark columns that are used in showitem or palettes for further processing
*/
class TcaColumnsProcessShowitem implements FormDataProviderInterface
{
/**
* Determine which fields are shown to the user and add those to the list of
* columns that must be processed by the next data providers.
*
* @return array
*/
public function addData(array $result)
{
$recordTypeValue = $result['recordTypeValue'];
if (!isset($result['processedTca']['types'][$recordTypeValue]['showitem'])
|| !is_string($result['processedTca']['types'][$recordTypeValue]['showitem'])
) {
throw new \UnexpectedValueException(
'No or invalid showitem definition in TCA table ' . $result['tableName'] . ' for type ' . $recordTypeValue,
1438614542
);
}
if (empty($result['processedTca']['columns'])) {
// We are sure this is an array by InitializeProcessedTca data provider
return $result;
}
if ($result['isInlineChild'] && !$result['isInlineChildExpanded']) {
// If the record is an inline child that is not expanded, it is not necessary to calculate all fields
return $result;
}
$showItemFieldString = $result['processedTca']['types'][$recordTypeValue]['showitem'];
$showItemFieldArray = GeneralUtility::trimExplode(',', $showItemFieldString, true);
foreach ($showItemFieldArray as $fieldConfigurationString) {
$fieldConfigurationArray = GeneralUtility::trimExplode(';', $fieldConfigurationString);
$fieldName = $fieldConfigurationArray[0];
if ($fieldName === '--div--') {
continue;
}
if ($fieldName === '--palette--') {
if (isset($fieldConfigurationArray[2])) {
$paletteName = $fieldConfigurationArray[2];
if (!empty($result['processedTca']['palettes'][$paletteName]['showitem'])) {
$paletteFields = GeneralUtility::trimExplode(',', $result['processedTca']['palettes'][$paletteName]['showitem'], true);
foreach ($paletteFields as $paletteFieldConfiguration) {
$paletteFieldConfigurationArray = GeneralUtility::trimExplode(';', $paletteFieldConfiguration);
$result['columnsToProcess'][] = $paletteFieldConfigurationArray[0];
}
}
}
} else {
$result['columnsToProcess'][] = $fieldName;
}
}
return $result;
}
}
@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* Remove or disable fields (select, category, language) in the form when no
* selectable items are available, or when only a single choice exists (language
* fields).
*
* For regular users, the field is removed entirely. When backend debug mode is
* enabled ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] = true), the field is
* kept as readOnly with an info badge, so admins can identify configuration
* issues such as missing records or restrictive TSconfig.
*
* Existing database values are not affected: fields not rendered in the form
* are simply not submitted, so DataHandler preserves their stored values.
*
* Fields can opt out via TCA config 'showIfEmpty' => true.
*/
readonly class TcaColumnsRemoveEmptyRelations implements FormDataProviderInterface
{
public function addData(array $result): array
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (!$this->isApplicableField($fieldConfig)) {
continue;
}
if ($fieldConfig['config']['showIfEmpty'] ?? false) {
continue;
}
$type = $fieldConfig['config']['type'] ?? '';
if ($type === 'language') {
if ($this->hasMultipleLanguageChoices($fieldConfig)) {
continue;
}
} elseif ($this->hasMeaningfulItems($fieldConfig)) {
continue;
}
if ($this->getBackendUser()->shallDisplayDebugInformation()) {
$result['processedTca']['columns'][$fieldName]['config']['readOnly'] = true;
$result['processedTca']['columns'][$fieldName]['config']['fieldInformation']['noSelectableItemsAvailable'] = [
'renderType' => 'noSelectableItemsAvailable',
];
} else {
unset($result['processedTca']['columns'][$fieldName]);
}
}
return $result;
}
/**
* Check if this field type should be handled by this provider.
*/
private function isApplicableField(array $fieldConfig): bool
{
return in_array($fieldConfig['config']['type'] ?? '', ['select', 'category', 'language'], true);
}
/**
* Check if the field has meaningful selectable items.
*
* For fields with foreign_table, only items from the foreign table (positive
* integer UIDs) count. Static items like "Hide at login" (-1) or dividers
* are not meaningful on their own — they require actual foreign records to
* be useful (e.g., access restriction options require fe_groups to exist,
* otherwise there can be no frontend login).
*/
private function hasMeaningfulItems(array $fieldConfig): bool
{
$items = $fieldConfig['config']['items'] ?? [];
$hasForeignTable = !empty($fieldConfig['config']['foreign_table']);
foreach ($items as $item) {
$value = (string)($item['value'] ?? $item[1] ?? '');
if ($value === '' || $value === '--div--') {
continue;
}
if ($hasForeignTable) {
if ((int)$value > 0) {
return true;
}
} else {
return true;
}
}
return false;
}
/**
* Check if a language field has more than one real language choice.
* Filters out --div-- separators and the -1 "All languages" special item,
* since on a single-language site neither provides a meaningful choice.
*/
private function hasMultipleLanguageChoices(array $fieldConfig): bool
{
$items = $fieldConfig['config']['items'] ?? [];
$realLanguageCount = 0;
foreach ($items as $item) {
$value = $item['value'] ?? '';
if ((string)$value === '--div--' || (int)$value === -1) {
continue;
}
$realLanguageCount++;
if ($realLanguageCount > 1) {
return true;
}
}
return false;
}
private function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,42 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Remove fields from columns not in showitem or palette list or needed otherwise
* This is a relatively effective performance improvement preventing other
* providers from resolving stuff of fields that are not shown later.
* Especially effective for fal related tables.
*/
class TcaColumnsRemoveUnused implements FormDataProviderInterface
{
/**
* Remove unused column fields to speed up further processing.
*
* @return array
*/
public function addData(array $result)
{
$columnsToRemove = array_diff(array_keys($result['processedTca']['columns']), $result['columnsToProcess']);
foreach ($columnsToRemove as $column) {
unset($result['processedTca']['columns'][$column]);
}
return $result;
}
}
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/*
* 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 Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Form\Processor\SelectItemProcessor;
use TYPO3\CMS\Core\Country\CountryFilter;
use TYPO3\CMS\Core\Country\CountryProvider;
/**
* Resolve select items for the type="country" and set processed item list in processedTca
*/
#[Autoconfigure(public: true)]
final class TcaCountry extends AbstractItemProvider implements FormDataProviderInterface
{
public function __construct(
private readonly CountryProvider $countryProvider,
private readonly SelectItemProcessor $selectItemProcessor,
) {}
/**
* Fetch countries to add them as select item
*
* @throws \UnexpectedValueException
*/
public function addData(array $result): array
{
$table = $result['tableName'];
$languageService = $this->getLanguageService();
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (!isset($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'country') {
continue;
}
$allItems = [];
$filter = new CountryFilter($fieldConfig['config']['filter']['excludeCountries'] ?? [], $fieldConfig['config']['filter']['onlyCountries'] ?? []);
$allCountries = $this->countryProvider->getFiltered($filter);
foreach ($allCountries as $country) {
$code = $country->getAlpha2IsoCode();
switch ($fieldConfig['config']['labelField'] ?? 'name') {
case 'localizedName':
$allItems[$code] = $languageService->sL($country->getLocalizedNameLabel());
break;
case 'name':
$allItems[$code] = $country->getName();
break;
case 'iso2':
$allItems[$code] = $country->getAlpha2IsoCode();
break;
case 'iso3':
$allItems[$code] = $country->getAlpha3IsoCode();
break;
case 'officialName':
$allItems[$code] = $country->getOfficialName() ?? $country->getName();
break;
case 'localizedOfficialName':
$name = $languageService->sL($country->getLocalizedOfficialNameLabel());
if (!$name) {
$name = $languageService->sL($country->getLocalizedNameLabel());
}
$allItems[$code] = $name;
break;
default:
throw new \UnexpectedValueException(
'Setting "labelField" must either be set to "localizedName", "name", "iso2", "iso3", "officialName", or "localizedOfficialName".',
1675895616
);
}
}
$prioritizedItems = [];
if (is_array($fieldConfig['config']['prioritizedCountries'] ?? false) && !empty($fieldConfig['config']['prioritizedCountries'])) {
foreach ($fieldConfig['config']['prioritizedCountries'] as $countryCode) {
if (isset($allItems[$countryCode])) {
$label = $allItems[$countryCode];
$prioritizedItems[$countryCode] = $label;
unset($allItems[$countryCode]);
}
}
}
$items = [];
$useItemGroups = !empty($prioritizedItems);
// When not required, prefix an empty value
if (!($fieldConfig['config']['required'] ?? false)) {
$items[''] = '';
}
$this->addItem($items, $prioritizedItems, $allCountries, $useItemGroups ? 'prioritized' : '');
$this->addItem($items, $allItems, $allCountries, $useItemGroups ? 'default' : '');
$fieldConfig['config']['items'] = $items;
$itemGroups = [
'prioritized' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:countries.prioritized',
'default' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:countries.default',
];
// Respect TSconfig options
$fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->addItemsFromPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
// In case no items are set at this point, we can write this back and continue with the next column
if ($fieldConfig['config']['items'] === []) {
$result['processedTca']['columns'][$fieldName] = $fieldConfig;
continue;
}
// Translate labels
$fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName);
$fieldConfig['config']['items'] = $this->selectItemProcessor->groupAndSortItems(
$fieldConfig['config']['items'],
$itemGroups,
$fieldConfig['config']['sortItems'] ?? []
);
$result['processedTca']['columns'][$fieldName] = $fieldConfig;
}
return $result;
}
private function addItem(array &$items, array $list, array $allCountries, string $group = ''): void
{
foreach ($list as $key => $label) {
$option = [
'label' => $label,
'value' => $key,
'icon' => ($key !== '') ? 'flags-' . strtolower($allCountries[$key]->getAlpha2IsoCode()) : '',
];
if ($group !== '') {
$option['group'] = $group;
}
$items[] = $option;
}
}
}
+385
View File
@@ -0,0 +1,385 @@
<?php
declare(strict_types=1);
/*
* 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 Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Resolve and prepare files data.
*/
class TcaFiles extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
private const string FILE_REFERENCE_TABLE = 'sys_file_reference';
private const string FOREIGN_SELECTOR = 'uid_local';
public function __construct(
private readonly InlineStackProcessor $inlineStackProcessor,
private readonly LoggerInterface $logger,
) {}
public function addData(array $result): array
{
// inlineFirstPid is currently resolved by TcaInline
// @todo check if duplicating the functionality makes sense to resolve dependencies
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (($fieldConfig['config']['type'] ?? '') !== 'file') {
continue;
}
if (!$this->getBackendUser()->check('tables_modify', self::FILE_REFERENCE_TABLE)) {
// Early return if user is not allowed to modify the file reference table
continue;
}
if (!$result['tcaSchemata']->has(self::FILE_REFERENCE_TABLE)) {
throw new \RuntimeException('Table ' . self::FILE_REFERENCE_TABLE . ' does not exists', 1664364262);
}
$fileReferenceSchema = $result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE);
if (!$fileReferenceSchema->hasField(self::FOREIGN_SELECTOR)) {
throw new \RuntimeException('Table ' . self::FILE_REFERENCE_TABLE . ' has no column ' . self::FOREIGN_SELECTOR, 1770975128);
}
$childField = $fileReferenceSchema->getField(self::FOREIGN_SELECTOR);
if ($childField->getType() !== 'group' || !($childField->getConfiguration()['allowed'] ?? false)) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points to field '
. self::FOREIGN_SELECTOR . ' of table ' . self::FILE_REFERENCE_TABLE . ', but this field '
. 'is either not defined, is not of type "group" or does not define the "allowed" option.',
1664364263
);
}
$result['processedTca']['columns'][$fieldName]['children'] = [];
$result = $this->initializeMinMaxItems($result, $fieldName);
$result = $this->initializeParentSysLanguageUid($result, $fieldName);
$result = $this->initializeAppearance($result, $fieldName);
// If field is set to readOnly, set all fields of the relation to readOnly as well
if ($result['inlineParentConfig']['readOnly'] ?? false) {
foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
$result['processedTca']['columns'][$columnName]['config']['readOnly'] = true;
}
}
// Resolve existing file references - this is usually always done except on ajax calls
if ($result['inlineResolveExistingChildren']) {
$result = $this->resolveFileReferences($result, $fieldName);
if (!empty($fieldConfig['config']['selectorOrUniqueConfiguration'])) {
throw new \RuntimeException('selectorOrUniqueConfiguration not implemented for TCA type "file"', 1664380909);
}
}
}
return $result;
}
protected function initializeMinMaxItems(array $result, string $fieldName): array
{
$config = $result['processedTca']['columns'][$fieldName]['config'];
$config['minitems'] = isset($config['minitems']) ? MathUtility::forceIntegerInRange($config['minitems'], 0) : 0;
$config['maxitems'] = isset($config['maxitems']) ? MathUtility::forceIntegerInRange($config['maxitems'], 1) : 99999;
$result['processedTca']['columns'][$fieldName]['config'] = $config;
return $result;
}
protected function initializeParentSysLanguageUid(array $result, string $fieldName): array
{
if (($parentLanguageFieldName = (string)($result['processedTca']['ctrl']['languageField'] ?? '')) === ''
|| !$result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE)->hasCapability(TcaSchemaCapability::Language)
|| isset($result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid'])
|| !isset($result['databaseRow'][$parentLanguageFieldName])
) {
return $result;
}
$result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid']
= is_array($result['databaseRow'][$parentLanguageFieldName])
? (int)($result['databaseRow'][$parentLanguageFieldName][0] ?? 0)
: (int)$result['databaseRow'][$parentLanguageFieldName];
return $result;
}
protected function initializeAppearance(array $result, string $fieldName): array
{
$result['processedTca']['columns'][$fieldName]['config']['appearance'] = array_replace_recursive(
[
'useSortable' => true,
'headerThumbnail' => [
'height' => '45m',
],
'enabledControls' => [
'edit' => true,
'info' => true,
'dragdrop' => true,
'sort' => false,
'hide' => true,
'delete' => true,
'localize' => true,
],
],
$result['processedTca']['columns'][$fieldName]['config']['appearance'] ?? []
);
return $result;
}
/**
* Substitute the value in databaseRow of this inline field with an array
* that contains the databaseRows of currently connected records and some meta information.
*/
protected function resolveFileReferences(array $result, string $fieldName): array
{
if ($result['defaultLanguageRow'] !== null) {
return $this->resolveFileReferenceOverlays($result, $fieldName);
}
$fileReferenceUidsOfDefaultLanguageRecord = $this->resolveFileReferenceUids(
$result['processedTca']['columns'][$fieldName]['config'],
$result['tableName'],
$result['databaseRow']['uid'],
$result['databaseRow'][$fieldName]
);
$result['databaseRow'][$fieldName] = implode(',', $fileReferenceUidsOfDefaultLanguageRecord);
if ($result['inlineCompileExistingChildren']) {
$fileReferenceSchema = $result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE);
foreach ($this->getSubstitutedWorkspacedUids($fileReferenceUidsOfDefaultLanguageRecord, $fileReferenceSchema) as $uid) {
try {
$compiledFileReference = $this->compileFileReference($result, $fieldName, $uid);
$result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference;
} catch (DatabaseRecordException $e) {
// Nothing to do here, missing file reference is just not being rendered.
}
}
}
return $result;
}
/**
* Substitute the value in databaseRow of this file field with an array
* that contains the databaseRows of currently connected file references
* and some meta information.
*/
protected function resolveFileReferenceOverlays(array $result, string $fieldName): array
{
$fileReferenceUidsOfLocalizedOverlay = [];
$fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
if ($result['command'] === 'edit') {
$fileReferenceUidsOfLocalizedOverlay = $this->resolveFileReferenceUids(
$fieldConfig,
$result['tableName'],
$result['databaseRow']['uid'],
$result['databaseRow'][$fieldName]
);
}
$result['databaseRow'][$fieldName] = implode(',', $fileReferenceUidsOfLocalizedOverlay);
$fileReferenceSchema = $result['tcaSchemata']->get(self::FILE_REFERENCE_TABLE);
$fileReferenceUidsOfLocalizedOverlay = $this->getSubstitutedWorkspacedUids($fileReferenceUidsOfLocalizedOverlay, $fileReferenceSchema);
if ($result['inlineCompileExistingChildren']) {
$tableNameWithDefaultRecords = $result['tableName'];
$fileReferenceUidsOfDefaultLanguageRecord = $this->getSubstitutedWorkspacedUids(
$this->resolveFileReferenceUids(
$fieldConfig,
$tableNameWithDefaultRecords,
$result['defaultLanguageRow']['uid'],
$result['defaultLanguageRow'][$fieldName]
),
$fileReferenceSchema
);
// Find which records are localized, which records are not localized and which are localized but miss default language record
$fieldNameWithDefaultLanguageUid = $fileReferenceSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName() ?? '';
$showPossibleLocalizationRecords = $fieldConfig['appearance']['showPossibleLocalizationRecords'] ?? false;
foreach ($fileReferenceUidsOfLocalizedOverlay as $localizedUid) {
try {
$localizedRecord = $this->getRecordFromDatabase(self::FILE_REFERENCE_TABLE, $localizedUid);
} catch (DatabaseRecordException $e) {
// The child could not be compiled, probably it was deleted and a dangling mm record exists
$this->logger->warning(
$e->getMessage(),
[
'table' => self::FILE_REFERENCE_TABLE,
'uid' => $localizedUid,
'exception' => $e,
]
);
continue;
}
// Compile localized record
$compiledFileReference = $this->compileFileReference($result, $fieldName, $localizedUid);
$result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference;
// If that relation is configured to "showPossibleLocalizationRecords", this localized record
// needs to be removed from the list of records that are pending to be localized.
if ($showPossibleLocalizationRecords) {
$uidOfDefaultLanguageRecord = (int)$localizedRecord[$fieldNameWithDefaultLanguageUid];
if (in_array($uidOfDefaultLanguageRecord, $fileReferenceUidsOfDefaultLanguageRecord, true)) {
// This localized child has a default language record. Remove this record from list of default language records
$fileReferenceUidsOfDefaultLanguageRecord = array_diff($fileReferenceUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecord]);
}
$uidOfDefaultLanguageRecordWorkspaceVersionArray = $this->getSubstitutedWorkspacedUids([$uidOfDefaultLanguageRecord], $fileReferenceSchema);
if (!empty($uidOfDefaultLanguageRecordWorkspaceVersionArray)
&& in_array($uidOfDefaultLanguageRecordWorkspaceVersionArray[0], $fileReferenceUidsOfDefaultLanguageRecord, true)
) {
// In some situations 'l10n_parent' of a localized workspace record points to the live version
// of the default language record, and not to the workspace version, even though it exists.
// Filter those as well, since the interface would otherwise show the item as "can be localized/synchronized".
$fileReferenceUidsOfDefaultLanguageRecord = array_diff($fileReferenceUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecordWorkspaceVersionArray[0]]);
}
}
}
if ($showPossibleLocalizationRecords) {
foreach ($fileReferenceUidsOfDefaultLanguageRecord as $defaultLanguageUid) {
// If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that
// exist in default language, but are not localized yet. Compile and mark those
try {
$compiledFileReference = $this->compileFileReference($result, $fieldName, $defaultLanguageUid, true);
} catch (DatabaseRecordException $e) {
// The child could not be compiled, probably it was deleted and a dangling mm record exists
$this->logger->warning(
$e->getMessage(),
[
'table' => self::FILE_REFERENCE_TABLE,
'uid' => $defaultLanguageUid,
'exception' => $e,
]
);
continue;
}
$result['processedTca']['columns'][$fieldName]['children'][] = $compiledFileReference;
}
}
}
return $result;
}
protected function compileFileReference(array $result, string $parentFieldName, int $childUid, $isInlineDefaultLanguageRecordInLocalizedParentContext = false): array
{
$inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($result['inlineStructure'], 0) ?: [];
return GeneralUtility::makeInstance(FormDataCompiler::class)
->compile(
[
'request' => $result['request'],
'command' => 'edit',
'tableName' => self::FILE_REFERENCE_TABLE,
'vanillaUid' => $childUid,
'returnUrl' => $result['returnUrl'],
'isInlineChild' => true,
'isInlineDefaultLanguageRecordInLocalizedParentContext' => $isInlineDefaultLanguageRecordInLocalizedParentContext,
'inlineStructure' => $result['inlineStructure'],
'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
'inlineFirstPid' => $result['inlineFirstPid'],
'inlineParentConfig' => $result['processedTca']['columns'][$parentFieldName]['config'],
'inlineParentUid' => $result['databaseRow']['uid'],
'inlineParentTableName' => $result['tableName'],
'inlineParentFieldName' => $parentFieldName,
'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'] ?? '',
'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'] ?? '',
'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'] ?? '',
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
],
GeneralUtility::makeInstance(TcaDatabaseRecord::class)
);
}
/**
* Substitute given list of uids with corresponding workspace uids - if needed
*
* @param int[] $connectedUids List of file reference uids
* @return int[] List of substituted uids
*/
protected function getSubstitutedWorkspacedUids(array $connectedUids, TcaSchema $fileReferenceSchema): array
{
$workspace = $this->getBackendUser()->workspace;
if ($workspace === 0 || !$fileReferenceSchema->hasCapability(TcaSchemaCapability::Workspace)) {
return $connectedUids;
}
$substitutedUids = [];
foreach ($connectedUids as $uid) {
$workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord(
$workspace,
self::FILE_REFERENCE_TABLE,
$uid,
'uid,t3ver_state'
);
if (!empty($workspaceVersion)) {
$versionState = VersionState::tryFrom($workspaceVersion['t3ver_state'] ?? 0);
if ($versionState === VersionState::DELETE_PLACEHOLDER) {
continue;
}
$uid = $workspaceVersion['uid'];
}
$substitutedUids[] = (int)$uid;
}
return $substitutedUids;
}
/**
* Resolve file reference uids using the RelationHandler
*
* @return int[]
*/
protected function resolveFileReferenceUids(
array $parentConfig,
$parentTableName,
$parentUid,
$parentFieldValue
): array {
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$relationHandler->start(
$parentFieldValue,
self::FILE_REFERENCE_TABLE,
'',
BackendUtility::getLiveVersionIdOfRecord($parentTableName, $parentUid) ?? $parentUid,
$parentTableName,
$parentConfig
);
return array_map(intval(...), $relationHandler->getValueArray());
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidDataStructureException;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidIdentifierException;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaException;
use TYPO3\CMS\Core\Configuration\FlexForm\Exception\InvalidTcaSchemaException;
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Schema\Exception\UndefinedSchemaException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Resolve flex data structure and data values, prepare and normalize.
*
* This is the first data provider in the chain of flex form related providers.
*/
readonly class TcaFlexPrepare implements FormDataProviderInterface
{
public function __construct(
private FlexFormTools $flexFormTools,
) {}
/**
* Resolve flex data structures and prepare flex data values.
*
* Normalize some details to have aligned array nesting for the rest of the
* processing method and the render engine.
*/
public function addData(array $result): array
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'flex') {
continue;
}
$result = $this->initializeDataStructure($result, (string)$fieldName);
$result = $this->initializeDataValues($result, (string)$fieldName);
}
return $result;
}
/**
* Fetch / initialize data structure.
*
* The data structures in ['config']['ds'] is initialized here and the dataStructureIdentifier is set.
*/
protected function initializeDataStructure(array $result, string $fieldName): array
{
$dataStructureArray = ['sheets' => ['sDEF' => []]];
if (!isset($result['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'])) {
try {
// Actually ['config']['ds'] might already contain the resolved data structure. However,
// since the references value might be a file path and a couple of events exist for flex
// form resolving, we nevertheless need to call getDataStructureIdentifier() and
// parseDataStructureByIdentifier() here.
$schema = $result['tcaSchemata']->get($result['tableName']);
$dataStructureIdentifier = $this->flexFormTools->getDataStructureIdentifier(
$result['processedTca']['columns'][$fieldName],
$result['tableName'],
$fieldName,
$result['databaseRow'],
$schema
);
$dataStructureArray = $this->flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier, $schema);
// Add the identifier to TCA to use it later during rendering
$result['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'] = $dataStructureIdentifier;
} catch (InvalidDataStructureException|InvalidIdentifierException|InvalidTcaException|UndefinedSchemaException) {
// Skip the data structure if it is invalid
}
} elseif (is_array($result['processedTca']['columns'][$fieldName]['config']['ds'] ?? false)) {
// Data structure has been given from outside
$dataStructureArray = $result['processedTca']['columns'][$fieldName]['config']['ds'];
} else {
// Resolve data structure base on given dataStructureIdentifier
try {
$dataStructureArray = $this->flexFormTools->parseDataStructureByIdentifier($result['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'], $result['tcaSchemata']->get($result['tableName']));
} catch (InvalidDataStructureException|InvalidIdentifierException|InvalidTcaSchemaException|UndefinedSchemaException) {
// Skip the data structure if it is invalid
}
}
if (!isset($dataStructureArray['meta']) || !is_array($dataStructureArray['meta'])) {
$dataStructureArray['meta'] = [];
}
// Finally add the resolved data Structure to "ds"
$result['processedTca']['columns'][$fieldName]['config']['ds'] = $dataStructureArray;
return $result;
}
/**
* Parse / initialize value from xml string to array
*/
protected function initializeDataValues(array $result, string $fieldName): array
{
$valueArray = [];
if (isset($result['databaseRow'][$fieldName]) && $result['databaseRow'][$fieldName] !== '') {
if (is_array($result['databaseRow'][$fieldName])) {
$valueArray = $result['databaseRow'][$fieldName];
} else {
$valueArray = GeneralUtility::xml2array($result['databaseRow'][$fieldName]);
if (!is_array($valueArray)) {
$valueArray = [];
}
}
}
$valueArray['data'] ??= [];
$valueArray['meta'] ??= [];
$result['databaseRow'][$fieldName] = $valueArray;
return $result;
}
}
@@ -0,0 +1,598 @@
<?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 TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\FlexFormSegment;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Process data structures and data values, calculate defaults.
*
* This is typically the last provider, executed after TcaFlexPrepare
*/
class TcaFlexProcess implements FormDataProviderInterface
{
/**
* Determine possible pageTsConfig overrides and apply them to ds.
* Determine available languages and sanitize ds for further processing. Then kick
* and validate further details like excluded fields. Finally, for each possible
* value and ds, call FormDataCompiler with set FlexFormSegment group to resolve
* single field stuff like item processor functions.
*/
public function addData(array $result): array
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'flex') {
continue;
}
if (!isset($result['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'])) {
throw new \RuntimeException(
'Data structure identifier must be set, typically by executing TcaFlexPrepare data provider before',
1480765571
);
}
$dataStructureIdentifier = $result['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'];
$simpleDataStructureIdentifier = $this->getSimplifiedDataStructureIdentifier($dataStructureIdentifier);
$pageTsConfigOfFlex = $this->getPageTsOfFlex($result, $fieldName, $simpleDataStructureIdentifier);
$result = $this->modifyOuterDataStructure($result, $fieldName, $pageTsConfigOfFlex);
$result = $this->removeExcludeFieldsFromDataStructure($result, $fieldName, $simpleDataStructureIdentifier);
$result = $this->mergeFieldDefinitionWithPageTsConfig($result, $fieldName, $pageTsConfigOfFlex);
$result = $this->removeDisabledFieldsFromDataStructure($result, $fieldName, $pageTsConfigOfFlex);
// A "normal" call opening a record: Process data structure and field values
// This is called for "new" container ajax request too, since display conditions from section container
// elements can access record values of other flex form sheets and we need their values then.
$result = $this->modifyDataStructureAndDataValuesByFlexFormSegmentGroup($result, $fieldName, $pageTsConfigOfFlex);
if (!empty($result['flexSectionContainerPreparation'])) {
// Create data and default values for a new section container, set by FormFlexAjaxController
$result = $this->prepareNewSectionContainer($result, $fieldName);
}
}
return $result;
}
/**
* Calculate a simplified (and wrong) data structure identifier.
* This is used to find pageTsConfig options of flex fields and exclude field definitions later, see methods below.
* If the data structure identifier is not type=tca based and if dataStructureKey is not as expected, fallback is "default"
*
* Example pi_flexform with ext:news in tt_content:
* * CType in databaseRow is "news_pi1"
* * The resulting dataStructureIdentifier calculated by FlexFormTools is then:
* {"type":"tca","tableName":"tt_content","fieldName":"pi_flexform","dataStructureKey":"news_pi1"}
* * The resulting simpleDataStructureIdentifier is "news_pi1"
* * The pageTsConfig base path used for flex field overrides is "TCEFORM.tt_content.pi_flexform.news_pi1", a full
* example path disabling a field: "TCEFORM.tt_content.pi_flexform.news_pi1.sDEF.settings\.orderBy.disabled = 1"
* * The exclude path for be_user exclude rights is "tt_content:pi_flexform;news_pi1", a full example:
* tt_content:pi_flexform;news_pi1;sDEF;settings.orderBy
*
* Notes:
* This approach is obviously limited. It is not possible to override flex form DS via pageTsConfig for other complex
* or dynamically created data structure definitions. And worse, the fallback to "default" may lead to naming clashes
* if two different data structures have identical sheet and field names.
* Also, the exclude field handling is limited, and it is not possible to respect 'exclude' fields in flex form
* data structures if the dataStructureIdentifier is based on type="record" or manipulated by a hook in FlexFormTools.
* All that can only be solved by changing the pageTsConfig syntax referencing flex fields, probably by involving the whole
* data structure identifier and going away from this "simple" approach. For exclude fields there is the additional
* issue that the special="exclude" code is based on guess work, to find possible data structures. If this area here is
* changed and a pageTsConfig syntax change is raised, it would probably be a good idea to solve the access restrictions
* area at the same time - see the related methods that deal with flex field handling for special="exclude" for
* more comments on this.
* Another limitation is that the current syntax in both pageTsConfig and exclude fields does not
* consider flex form section containers at all.
*/
protected function getSimplifiedDataStructureIdentifier(string $dataStructureIdentifier): string
{
$identifierArray = json_decode($dataStructureIdentifier, true);
$simpleDataStructureIdentifier = 'default';
if (isset($identifierArray['type'], $identifierArray['dataStructureKey'])
&& $identifierArray['type'] === 'tca'
&& $identifierArray['dataStructureKey'] !== ''
) {
$simpleDataStructureIdentifier = $identifierArray['dataStructureKey'];
}
return $simpleDataStructureIdentifier;
}
/**
* Determine TCEFORM.aTable.aField.matchingIdentifier
*
* @param array $result Result array
* @param string $fieldName Handled field name
* @param string $flexIdentifier Determined identifier
* @return array Page TSconfig for this flex
*/
protected function getPageTsOfFlex(array $result, $fieldName, $flexIdentifier)
{
$table = $result['tableName'];
$pageTs = [];
if (!empty($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'][$flexIdentifier . '.'])
&& is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'][$flexIdentifier . '.'])) {
$pageTs = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'][$flexIdentifier . '.'];
}
return $pageTs;
}
/**
* Handle "outer" flex data structure changes like language and sheet
* description. Does not change "TCA" or values of single elements
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @param array $pageTsConfig Given pageTsConfig of this flex form
* @return array Modified item array
*/
protected function modifyOuterDataStructure(array $result, $fieldName, $pageTsConfig)
{
$modifiedDataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
if (isset($modifiedDataStructure['sheets']) && is_array($modifiedDataStructure['sheets'])) {
// Handling multiple sheets
foreach ($modifiedDataStructure['sheets'] as $sheetName => $sheetStructure) {
if (isset($pageTsConfig[$sheetName . '.']) && is_array($pageTsConfig[$sheetName . '.'])) {
$pageTsOfSheet = $pageTsConfig[$sheetName . '.'];
// Remove whole sheet if disabled
if (!empty($pageTsOfSheet['disabled'])) {
unset($modifiedDataStructure['sheets'][$sheetName]);
continue;
}
// sheetTitle, sheetDescription, sheetShortDescr
$modifiedDataStructure['sheets'][$sheetName] = $this->modifySingleSheetInformation($sheetStructure, $pageTsOfSheet);
}
}
}
$result['processedTca']['columns'][$fieldName]['config']['ds'] = $modifiedDataStructure;
return $result;
}
/**
* Removes fields from data structure the user has no access to
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @param string $flexIdentifier Determined identifier
* @return array Modified result
*/
protected function removeExcludeFieldsFromDataStructure(array $result, $fieldName, $flexIdentifier)
{
$dataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
$backendUser = $this->getBackendUser();
if ($backendUser->isAdmin() || !isset($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
return $result;
}
$userNonExcludeFields = GeneralUtility::trimExplode(',', $backendUser->groupData['non_exclude_fields']);
$excludeFieldsPrefix = $result['tableName'] . ':' . $fieldName . ';' . $flexIdentifier . ';';
$nonExcludeFields = [];
foreach ($userNonExcludeFields as $userNonExcludeField) {
if (str_contains($userNonExcludeField, $excludeFieldsPrefix)) {
$exploded = explode(';', $userNonExcludeField);
$sheetName = $exploded[2];
$allowedFlexFieldName = $exploded[3];
$nonExcludeFields[$sheetName][$allowedFlexFieldName] = true;
}
}
foreach ($dataStructure['sheets'] as $sheetName => $sheetDefinition) {
if (!isset($sheetDefinition['ROOT']['el']) || !is_array($sheetDefinition['ROOT']['el'])) {
continue;
}
foreach ($sheetDefinition['ROOT']['el'] as $flexFieldName => $fieldDefinition) {
if (!empty($fieldDefinition['exclude']) && !isset($nonExcludeFields[$sheetName][$flexFieldName])) {
unset($result['processedTca']['columns'][$fieldName]['config']['ds']['sheets'][$sheetName]['ROOT']['el'][$flexFieldName]);
}
}
}
return $result;
}
/**
* Merge fields of FlexForm TCA with config of pageTSConfig
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @param array $pageTsConfig Given pageTsConfig of this flex form
* @return array Modified item array
*/
protected function mergeFieldDefinitionWithPageTsConfig(array $result, $fieldName, $pageTsConfig)
{
$dataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
foreach ($dataStructure['sheets'] ?? [] as $sheetName => $sheetDefinition) {
if (!isset($pageTsConfig[$sheetName . '.'])) {
continue;
}
foreach ($sheetDefinition['ROOT']['el'] ?? [] as $flexFieldName => $fieldDefinition) {
if (!isset($pageTsConfig[$sheetName . '.'][$flexFieldName . '.'])) {
continue;
}
// Override fieldConf by fieldTSconfig:
$result['processedTca']['columns'][$fieldName]['config']['ds']['sheets'][$sheetName]['ROOT']['el'][$flexFieldName]['config'] = FormEngineUtility::overrideFieldConf($fieldDefinition['config'], $pageTsConfig[$sheetName . '.'][$flexFieldName . '.'] ?? []);
}
}
return $result;
}
/**
* Remove fields from data structure that are disabled in pageTsConfig.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @param array $pageTsConfig Given pageTsConfig of this flex form
* @return array Modified item array
*/
protected function removeDisabledFieldsFromDataStructure(array $result, $fieldName, $pageTsConfig)
{
$dataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
if (!isset($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
return $result;
}
foreach ($dataStructure['sheets'] as $sheetName => $sheetDefinition) {
if (!isset($sheetDefinition['ROOT']['el']) || !is_array($sheetDefinition['ROOT']['el'])
|| !isset($pageTsConfig[$sheetName . '.'])) {
continue;
}
foreach ($sheetDefinition['ROOT']['el'] as $flexFieldName => $fieldDefinition) {
if (!empty($pageTsConfig[$sheetName . '.'][$flexFieldName . '.']['disabled'])) {
unset($result['processedTca']['columns'][$fieldName]['config']['ds']['sheets'][$sheetName]['ROOT']['el'][$flexFieldName]);
}
}
}
return $result;
}
/**
* Feed single flex field and data to FlexFormSegment FormData compiler and merge result.
* This one is nasty. Goal is to have processed TCA stuff in DS and also have validated / processed data values.
*
* Two main parts in this method:
* * Process values and TCA of existing section containers
* * Process TCA of "normal" fields
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @param array $pageTsConfig Given pageTsConfig of this flex form
* @return array Modified item array
* @throws \UnexpectedValueException
*/
protected function modifyDataStructureAndDataValuesByFlexFormSegmentGroup(array $result, $fieldName, $pageTsConfig)
{
$dataStructure = $result['processedTca']['columns'][$fieldName]['config']['ds'];
$dataValues = $result['databaseRow'][$fieldName];
$tableName = $result['tableName'];
if (!isset($dataStructure['sheets']) || !is_array($dataStructure['sheets'])) {
return $result;
}
$formDataGroup = GeneralUtility::makeInstance(FlexFormSegment::class);
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class);
foreach ($dataStructure['sheets'] as $dataStructureSheetName => $dataStructureSheetDefinition) {
if (!isset($dataStructureSheetDefinition['ROOT']['el']) || !is_array($dataStructureSheetDefinition['ROOT']['el'])) {
continue;
}
$dataStructureFields = $dataStructureSheetDefinition['ROOT']['el'];
// Prepare pageTsConfig of this sheet
$pageTsConfig['TCEFORM.'][$tableName . '.'] = [];
if (isset($pageTsConfig[$dataStructureSheetName . '.']) && is_array($pageTsConfig[$dataStructureSheetName . '.'])) {
$pageTsConfig['TCEFORM.'][$tableName . '.'] = $pageTsConfig[$dataStructureSheetName . '.'];
}
// List of "new" tca fields that have no value within the flexform, yet. Those will be compiled in one go later.
$tcaNewColumns = [];
// List of "edit" tca fields that have a value in flexform, already. Those will be compiled in one go later.
$tcaEditColumns = [];
// Contains the data values for the "edit" tca fields.
$tcaValueArray = [
'uid' => $result['databaseRow']['uid'],
];
foreach ($dataStructureFields as $dataStructureFieldName => $dataStructureFieldDefinition) {
if (isset($dataStructureFieldDefinition['type']) && $dataStructureFieldDefinition['type'] === 'array'
&& isset($dataStructureFieldDefinition['section']) && (string)$dataStructureFieldDefinition['section'] === '1'
) {
// Existing section containers. Prepare data values and create a unique data structure per container.
// This is important for instance for display conditions later enabling them to change ds per container instance.
// In the end, the data values in
// ['databaseRow']['aFieldName']['data']['aSheet']['lDEF']['aSectionField']['el']['aContainer']
// are prepared, and additionally, the processedTca data structure is changed and has a specific container
// name per container instance in
// ['processedTca']['columns']['aFieldName']['config']['ds']['sheets']['aSheet']['ROOT']['el']['aSectionField']['children']['aContainer']
if (isset($dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'])
&& is_array($dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'])
) {
$containerValueArray = $dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'];
$containerDataStructuresPerContainer = [];
foreach ($containerValueArray as $aContainerIdentifier => $aContainerArray) {
if (is_array($aContainerArray)) {
foreach ($aContainerArray as $aContainerName => $aContainerElementArray) {
if (!isset($dataStructureFields[$dataStructureFieldName]['el'][$aContainerName])) {
// Container not defined in ds
continue;
}
$vanillaContainerDataStructure = $dataStructureFields[$dataStructureFieldName]['el'][$aContainerName];
$newColumns = [];
$editColumns = [];
$valueArray = [
'uid' => $result['databaseRow']['uid'],
];
foreach ($vanillaContainerDataStructure['el'] as $singleFieldName => $singleFieldConfiguration) {
// $singleFieldValueArray = ['data']['sSections']['lDEF']['section_1']['el']['1']['container_1']['el']['element_1']
$singleFieldValueArray = [];
if (isset($aContainerElementArray['el'][$singleFieldName])
&& is_array($aContainerElementArray['el'][$singleFieldName])
) {
$singleFieldValueArray = $aContainerElementArray['el'][$singleFieldName];
}
if (array_key_exists('vDEF', $singleFieldValueArray)) {
$valueArray[$singleFieldName] = $singleFieldValueArray['vDEF'];
} else {
$newColumns[$singleFieldName] = $singleFieldConfiguration;
}
$editColumns[$singleFieldName] = $singleFieldConfiguration;
}
$inputToFlexFormSegment = [
'request' => $result['request'],
'tableName' => $result['tableName'],
'command' => '',
// It is currently not possible to have pageTsConfig for section container
'pageTsConfig' => [],
'databaseRow' => $valueArray,
'processedTca' => [
'ctrl' => [],
'columns' => [],
],
'selectTreeCompileItems' => $result['selectTreeCompileItems'],
'flexParentDatabaseRow' => $result['databaseRow'],
'effectivePid' => $result['effectivePid'],
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
];
if (!empty($newColumns)) {
// This is scenario "field has been added to data structure, but field value does not exist in value array yet"
// We want that stuff like TCA "default" values are then applied to those fields. What we do here is
// calling the data compiler with those "new" fields to fetch their values and set them in value array.
// Those fields are then compiled a second time in the "edit" phase to prepare their final TCA.
// This two-phase compiling is needed to ensure that for instance display conditions work with
// fields that may just have been added to the data structure but are not yet initialized as data value.
$inputToFlexFormSegment['command'] = 'new';
$inputToFlexFormSegment['processedTca']['columns'] = $newColumns;
$flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment, $formDataGroup);
foreach ($newColumns as $singleFieldName => $_) {
// Set data value result to feed it to "edit" next
$valueArray[$singleFieldName] = $flexSegmentResult['databaseRow'][$singleFieldName];
}
}
if (!empty($editColumns)) {
$inputToFlexFormSegment['command'] = 'edit';
$inputToFlexFormSegment['processedTca']['columns'] = $editColumns;
$flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment, $formDataGroup);
foreach ($editColumns as $singleFieldName => $_) {
$result['databaseRow'][$fieldName]
['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]
['el'][$aContainerIdentifier][$aContainerName]['el'][$singleFieldName]['vDEF']
= $flexSegmentResult['databaseRow'][$singleFieldName];
$containerDataStructuresPerContainer[$aContainerIdentifier] = $vanillaContainerDataStructure;
$containerDataStructuresPerContainer[$aContainerIdentifier]['el'] = $flexSegmentResult['processedTca']['columns'];
}
}
}
}
} // End of existing data value handling
// Set 'data structures per container' next to 'el' that contains vanilla data structures
$result['processedTca']['columns'][$fieldName]['config']['ds']
['sheets'][$dataStructureSheetName]['ROOT']['el']
[$dataStructureFieldName]['children'] = $containerDataStructuresPerContainer;
} else {
// Force the section data array to be an empty array if there are no existing containers
$result['databaseRow'][$fieldName]
['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['el'] = [];
// Force data structure array to be empty if there are no existing containers
$result['processedTca']['columns'][$fieldName]['config']['ds']
['sheets'][$dataStructureSheetName]['ROOT']['el']
[$dataStructureFieldName]['children'] = [];
}
} else {
// A "normal" TCA flex form element, no section
if (isset($dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName])
&& array_key_exists('vDEF', $dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName])
) {
$tcaEditColumns[$dataStructureFieldName] = $dataStructureFieldDefinition;
$tcaValueArray[$dataStructureFieldName] = $dataValues['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['vDEF'];
} else {
$tcaNewColumns[$dataStructureFieldName] = $dataStructureFieldDefinition;
}
} // End of single element handling
}
// process the tca columns for the current sheet
$inputToFlexFormSegment = [
'request' => $result['request'],
'tableName' => $result['tableName'],
'command' => '',
'pageTsConfig' => $pageTsConfig,
'databaseRow' => $tcaValueArray,
'processedTca' => [
'ctrl' => [],
'columns' => [],
],
'flexParentDatabaseRow' => $result['databaseRow'],
// Whether to compile TCA tree items - inherit from parent
'selectTreeCompileItems' => $result['selectTreeCompileItems'],
'effectivePid' => $result['effectivePid'],
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
];
if (!empty($tcaNewColumns)) {
// @todo: this has the same problem in scenario "a field was added later" as flex section container
$inputToFlexFormSegment['command'] = 'new';
$inputToFlexFormSegment['processedTca']['columns'] = $tcaNewColumns;
$flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment, $formDataGroup);
foreach ($tcaNewColumns as $dataStructureFieldName => $_) {
// Set data value result
if (array_key_exists($dataStructureFieldName, $flexSegmentResult['databaseRow'])) {
$result['databaseRow'][$fieldName]
['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['vDEF']
= $flexSegmentResult['databaseRow'][$dataStructureFieldName];
}
// Set TCA structure result
$result['processedTca']['columns'][$fieldName]['config']['ds']
['sheets'][$dataStructureSheetName]['ROOT']['el'][$dataStructureFieldName]
= $flexSegmentResult['processedTca']['columns'][$dataStructureFieldName];
}
}
if (!empty($tcaEditColumns)) {
$inputToFlexFormSegment['command'] = 'edit';
$inputToFlexFormSegment['processedTca']['columns'] = $tcaEditColumns;
$flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment, $formDataGroup);
foreach ($tcaEditColumns as $dataStructureFieldName => $_) {
// Set data value result
if (array_key_exists($dataStructureFieldName, $flexSegmentResult['databaseRow'])) {
$result['databaseRow'][$fieldName]
['data'][$dataStructureSheetName]['lDEF'][$dataStructureFieldName]['vDEF']
= $flexSegmentResult['databaseRow'][$dataStructureFieldName];
}
// Set TCA structure result
$result['processedTca']['columns'][$fieldName]['config']['ds']
['sheets'][$dataStructureSheetName]['ROOT']['el'][$dataStructureFieldName]
= $flexSegmentResult['processedTca']['columns'][$dataStructureFieldName];
}
}
}
return $result;
}
/**
* Prepare data structure and data values for a new section container.
*
* @param array $result Incoming result array
* @param string $fieldName The field name with this flex form
* @return array Modified result
*/
protected function prepareNewSectionContainer(array $result, string $fieldName): array
{
$flexSectionContainerPreparation = $result['flexSectionContainerPreparation'];
$flexFormSheetName = $flexSectionContainerPreparation['flexFormSheetName'];
$flexFormFieldName = $flexSectionContainerPreparation['flexFormFieldName'];
$flexFormContainerName = $flexSectionContainerPreparation['flexFormContainerName'];
$flexFormContainerIdentifier = $flexSectionContainerPreparation['flexFormContainerIdentifier'];
$containerConfiguration = $result['processedTca']['columns'][$fieldName]['config']['ds']
['sheets'][$flexFormSheetName]['ROOT']['el'][$flexFormFieldName]['el'][$flexFormContainerName] ?? [];
if (isset($containerConfiguration['el']) && is_array($containerConfiguration['el'])) {
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class);
$inputToFlexFormSegment = [
'request' => $result['request'],
'tableName' => $result['tableName'],
'command' => 'new',
// It is currently not possible to have pageTsConfig for section container
'pageTsConfig' => [],
'databaseRow' => [
'uid' => $result['databaseRow']['uid'],
],
'processedTca' => [
'ctrl' => [],
'columns' => $containerConfiguration['el'],
],
'selectTreeCompileItems' => $result['selectTreeCompileItems'],
'flexParentDatabaseRow' => $result['databaseRow'],
'effectivePid' => $result['effectivePid'],
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
];
$flexSegmentResult = $formDataCompiler->compile($inputToFlexFormSegment, GeneralUtility::makeInstance(FlexFormSegment::class));
foreach ($containerConfiguration['el'] as $singleFieldName => $singleFieldConfiguration) {
// Set 'data structures for this new container' to 'children'
$result['processedTca']['columns'][$fieldName]['config']['ds']
['sheets'][$flexFormSheetName]['ROOT']['el']
[$flexFormFieldName]['children'][$flexFormContainerIdentifier]
= $containerConfiguration;
$result['processedTca']['columns'][$fieldName]['config']['ds']
['sheets'][$flexFormSheetName]['ROOT']['el']
[$flexFormFieldName]['children'][$flexFormContainerIdentifier]['el']
= $flexSegmentResult['processedTca']['columns'];
// Set calculated value - this especially contains "default values from TCA"
$result['databaseRow'][$fieldName]['data'][$flexFormSheetName]['lDEF']
[$flexFormFieldName]['el']
[$flexFormContainerIdentifier][$flexFormContainerName]['el'][$singleFieldName]['vDEF']
= $flexSegmentResult['databaseRow'][$singleFieldName];
}
}
return $result;
}
/**
* Modify data structure of a single "sheet"
* Sets "secondary" data like sheet names and so on, but does NOT modify single elements
*
* @param array $dataStructure Given data structure
* @param array $pageTsOfSheet Page Ts config of given field
* @return array Modified data structure
*/
protected function modifySingleSheetInformation(array $dataStructure, array $pageTsOfSheet)
{
// Return if no elements defined
if (!isset($dataStructure['ROOT']['el']) || !is_array($dataStructure['ROOT']['el'])) {
return $dataStructure;
}
// Rename sheet (tab)
if (!empty($pageTsOfSheet['sheetTitle'])) {
$dataStructure['ROOT']['sheetTitle'] = $pageTsOfSheet['sheetTitle'];
}
// Set sheet description (tab)
if (!empty($pageTsOfSheet['sheetDescription'])) {
$dataStructure['ROOT']['sheetDescription'] = $pageTsOfSheet['sheetDescription'];
}
// Set sheet short description (tab)
if (!empty($pageTsOfSheet['sheetShortDescr'])) {
$dataStructure['ROOT']['sheetShortDescr'] = $pageTsOfSheet['sheetShortDescr'];
}
return $dataStructure;
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Resolve databaseRow field content for type=folder
*/
readonly class TcaFolder implements FormDataProviderInterface
{
public function __construct(
private ResourceFactory $resourceFactory,
) {}
/**
* Initialize new row with default values from various sources
*
* @return array
*/
public function addData(array $result)
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'folder') {
continue;
}
// Sanitize max items, set to 99999 if not defined
$result['processedTca']['columns'][$fieldName]['config']['maxitems'] = MathUtility::forceIntegerInRange(
$fieldConfig['config']['maxitems'] ?? 0,
0,
99999
);
if ($result['processedTca']['columns'][$fieldName]['config']['maxitems'] === 0) {
$result['processedTca']['columns'][$fieldName]['config']['maxitems'] = 99999;
}
$databaseRowFieldContent = '';
if (!empty($result['databaseRow'][$fieldName])) {
$databaseRowFieldContent = (string)$result['databaseRow'][$fieldName];
}
$items = [];
// Simple list of folders
$folderList = GeneralUtility::trimExplode(',', $databaseRowFieldContent, true);
foreach ($folderList as $folder) {
if (empty($folder)) {
continue;
}
try {
$folderObject = $this->resourceFactory->retrieveFileOrFolderObject($folder);
if ($folderObject instanceof Folder) {
$items[] = [
'folder' => $folder,
];
}
} catch (ResourceDoesNotExistException|InsufficientFolderAccessPermissionsException) {
continue;
}
}
$result['databaseRow'][$fieldName] = $items;
}
return $result;
}
}
+125
View File
@@ -0,0 +1,125 @@
<?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 TYPO3\CMS\Backend\Clipboard\Clipboard;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Resolve databaseRow field content to the real connected rows for type=group
*/
class TcaGroup implements FormDataProviderInterface
{
/**
* Initialize new row with default values from various sources
*
* @return array
*/
public function addData(array $result)
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'group') {
continue;
}
// Sanitize max items, set to 99999 if not defined
$result['processedTca']['columns'][$fieldName]['config']['maxitems'] = MathUtility::forceIntegerInRange(
$fieldConfig['config']['maxitems'] ?? 0,
0,
99999
);
if ($result['processedTca']['columns'][$fieldName]['config']['maxitems'] === 0) {
$result['processedTca']['columns'][$fieldName]['config']['maxitems'] = 99999;
}
$databaseRowFieldContent = '';
if (!empty($result['databaseRow'][$fieldName])) {
$databaseRowFieldContent = (string)$result['databaseRow'][$fieldName];
}
$items = [];
$sanitizedClipboardElements = [];
if (empty($fieldConfig['config']['allowed'])) {
throw new \RuntimeException(
'Mandatory TCA config setting "allowed" missing in field "' . $fieldName . '" of table "' . $result['tableName'] . '"',
1482250512
);
}
// In case of vanilla uid, 0 is used to query relations by splitting $databaseRowFieldContent (possible defVals)
$MMuid = MathUtility::canBeInterpretedAsInteger($result['databaseRow']['uid']) ? $result['databaseRow']['uid'] : 0;
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$relationHandler->start(
$databaseRowFieldContent,
$fieldConfig['config']['allowed'] ?? '',
$fieldConfig['config']['MM'] ?? '',
$MMuid,
$result['tableName'] ?? '',
$fieldConfig['config'] ?? []
);
$relationHandler->getFromDB();
$relationHandler->processDeletePlaceholder();
$relations = $relationHandler->getResolvedItemArray();
foreach ($relations as $relation) {
$tableName = $relation['table'];
$record = $relation['record'];
BackendUtility::workspaceOL($tableName, $record);
$title = BackendUtility::getRecordTitle($tableName, $record, false, false);
$items[] = [
'table' => $tableName,
'uid' => $record['uid'] ?? null,
'title' => $title,
'row' => $record,
];
}
// Register elements from clipboard
$allowed = GeneralUtility::trimExplode(',', $fieldConfig['config']['allowed'], true);
$clipboard = GeneralUtility::makeInstance(Clipboard::class);
$clipboard->initializeClipboard();
$clipboardElements = [];
if ($allowed[0] !== '*') {
// Only some tables, filter them:
foreach ($allowed as $tablename) {
$clipboardElements = [...$clipboardElements, ...array_keys($clipboard->elFromTable($tablename))];
}
} else {
// All tables allowed for relation:
$clipboardElements = array_keys($clipboard->elFromTable(''));
}
foreach ($clipboardElements as $elementValue) {
[$elementTable, $elementUid] = explode('|', $elementValue);
$record = BackendUtility::getRecordWSOL($elementTable, (int)$elementUid);
$sanitizedClipboardElements[] = [
'title' => BackendUtility::getRecordTitle($elementTable, $record),
'value' => $elementTable . '_' . $elementUid,
];
}
$result['databaseRow'][$fieldName] = $items;
$result['processedTca']['columns'][$fieldName]['config']['clipboardElements'] = $sanitizedClipboardElements;
}
return $result;
}
}
+538
View File
@@ -0,0 +1,538 @@
<?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 Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
use TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\RelationHandler;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Resolve and prepare inline data.
*/
class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
public function __construct(
private readonly FlashMessageService $flashMessageService,
private readonly InlineStackProcessor $inlineStackProcessor,
private readonly LoggerInterface $logger,
) {}
/**
* Resolve inline fields
*
* @return array
*/
public function addData(array $result)
{
$result = $this->addInlineFirstPid($result);
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (!$this->isInlineField($fieldConfig)) {
continue;
}
$result['processedTca']['columns'][$fieldName]['children'] = [];
if (!$this->isUserAllowedToModify($fieldConfig)) {
continue;
}
if ($result['inlineResolveExistingChildren']) {
$result = $this->resolveRelatedRecords($result, $fieldName);
$result = $this->addForeignSelectorAndUniquePossibleRecords($result, $fieldName);
}
}
return $result;
}
/**
* Is column of type "inline"
*
* @param array $fieldConfig
* @return bool
*/
protected function isInlineField($fieldConfig)
{
return !empty($fieldConfig['config']['type']) && $fieldConfig['config']['type'] === 'inline';
}
/**
* Is user allowed to modify child elements
*
* @param array $fieldConfig
* @return bool
*/
protected function isUserAllowedToModify($fieldConfig)
{
return $this->getBackendUser()->check('tables_modify', $fieldConfig['config']['foreign_table']);
}
/**
* The "entry" pid for inline records. Nested inline records can potentially hang around on different
* pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes place on the page structure.
*
* @param array $result Incoming result
* @return array Modified result
* @todo: Find out when and if this is different from 'effectivePid'
*/
protected function addInlineFirstPid(array $result)
{
if ($result['inlineFirstPid'] === null) {
$table = $result['tableName'];
$row = $result['databaseRow'];
// If the parent is a page, use the uid(!) of the (new?) page as pid for the child records:
if ($table === 'pages') {
$liveVersionId = BackendUtility::getLiveVersionIdOfRecord('pages', $row['uid']);
$pid = $liveVersionId ?? $row['uid'];
} elseif (($row['pid'] ?? 0) < 0) {
$prevRec = BackendUtility::getRecord($table, (int)abs($row['pid']));
$pid = $prevRec['pid'];
} else {
$pid = $row['pid'] ?? 0;
}
if (MathUtility::canBeInterpretedAsInteger($pid)) {
$pageRecord = BackendUtility::getRecord('pages', (int)$pid);
$pageSchema = $result['tcaSchemata']->get('pages');
if ($pageSchema->hasCapability(TcaSchemaCapability::Language)
&& ($pageRecord[$pageSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()] ?? 0) > 0) {
$pid = (int)$pageRecord[$pageSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName()];
}
} elseif (!str_starts_with($pid, 'NEW')) {
throw new \RuntimeException(
'inlineFirstPid should either be an integer or a "NEW..." string',
1521220142
);
}
$result['inlineFirstPid'] = $pid;
}
return $result;
}
/**
* Substitute the value in databaseRow of this inline field with an array
* that contains the databaseRows of currently connected records and some meta information.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
*/
protected function resolveRelatedRecordsOverlays(array $result, $fieldName)
{
$childTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
$connectedUidsOfLocalizedOverlay = [];
if ($result['command'] === 'edit') {
$connectedUidsOfLocalizedOverlay = $this->resolveConnectedRecordUids(
$result['processedTca']['columns'][$fieldName]['config'],
$result['tableName'],
$result['databaseRow'],
$result['databaseRow'][$fieldName]
);
}
$result['databaseRow'][$fieldName] = implode(',', $connectedUidsOfLocalizedOverlay);
$connectedUidsOfLocalizedOverlay = $this->getSubstitutedWorkspacedUids($result, $connectedUidsOfLocalizedOverlay, $childTableName);
if ($result['inlineCompileExistingChildren']) {
$tableNameWithDefaultRecords = $result['tableName'];
$connectedUidsOfDefaultLanguageRecord = $this->resolveConnectedRecordUids(
$result['processedTca']['columns'][$fieldName]['config'],
$tableNameWithDefaultRecords,
$result['defaultLanguageRow'],
(string)($result['defaultLanguageRow'][$fieldName] ?? '')
);
$connectedUidsOfDefaultLanguageRecord = $this->getSubstitutedWorkspacedUids($result, $connectedUidsOfDefaultLanguageRecord, $childTableName);
$showPossibleLocalizationRecords = $result['processedTca']['columns'][$fieldName]['config']['appearance']['showPossibleLocalizationRecords'] ?? false;
// Find which records are localized, which records are not localized and which are
// localized but miss default language record
$childTableSchema = $result['tcaSchemata']->has($childTableName) ? $result['tcaSchemata']->get($childTableName) : null;
$fieldNameWithDefaultLanguageUid = $childTableSchema?->hasCapability(TcaSchemaCapability::Language) ? $childTableSchema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName() : '';
foreach ($connectedUidsOfLocalizedOverlay as $localizedUid) {
try {
$localizedRecord = $this->getRecordFromDatabase($childTableName, $localizedUid);
} catch (DatabaseRecordException $e) {
// The child could not be compiled, probably it was deleted and a dangling mm record exists
$this->logger->warning(
$e->getMessage(),
[
'table' => $childTableName,
'uid' => $localizedUid,
'exception' => $e,
]
);
continue;
}
// Compile localized record
$compiledChild = $this->compileChild($result, $fieldName, $localizedUid);
$result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
// If that relation is configured to "showPossibleLocalizationRecords", this localized record
// needs to be removed from the list of records that are pending to be localized.
if ($fieldNameWithDefaultLanguageUid && $showPossibleLocalizationRecords) {
$uidOfDefaultLanguageRecord = (int)$localizedRecord[$fieldNameWithDefaultLanguageUid];
if (in_array($uidOfDefaultLanguageRecord, $connectedUidsOfDefaultLanguageRecord, true)) {
// This localized child has a default language record. Remove this record from list of default language records
$connectedUidsOfDefaultLanguageRecord = array_diff($connectedUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecord]);
}
$uidOfDefaultLanguageRecordWorkspaceVersionArray = $this->getSubstitutedWorkspacedUids($result, [$uidOfDefaultLanguageRecord], $childTableName);
if (!empty($uidOfDefaultLanguageRecordWorkspaceVersionArray)
&& in_array($uidOfDefaultLanguageRecordWorkspaceVersionArray[0], $connectedUidsOfDefaultLanguageRecord, true)
) {
// In some situations 'l10n_parent' of a localized workspace record points to the live version
// of the default language record, and not to the workspace version, even though it exists.
// Filter those as well, since the interface would otherwise show the item as "can be localized/synchronized".
$connectedUidsOfDefaultLanguageRecord = array_diff($connectedUidsOfDefaultLanguageRecord, [$uidOfDefaultLanguageRecordWorkspaceVersionArray[0]]);
}
}
}
if ($showPossibleLocalizationRecords) {
foreach ($connectedUidsOfDefaultLanguageRecord as $defaultLanguageUid) {
// If there are still uids in $connectedUidsOfDefaultLanguageRecord, these are records that
// exist in default language, but are not localized yet. Compile and mark those
try {
$compiledChild = $this->compileChild($result, $fieldName, $defaultLanguageUid, true);
} catch (DatabaseRecordException $e) {
// The child could not be compiled, probably it was deleted and a dangling mm record exists
$this->logger->warning(
$e->getMessage(),
[
'table' => $childTableName,
'uid' => $defaultLanguageUid,
'exception' => $e,
]
);
continue;
}
$result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
}
}
}
return $result;
}
/**
* Substitute the value in databaseRow of this inline field with an array
* that contains the databaseRows of currently connected records and some meta information.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
*/
protected function resolveRelatedRecords(array $result, $fieldName)
{
if ($result['defaultLanguageRow'] !== null) {
return $this->resolveRelatedRecordsOverlays($result, $fieldName);
}
$childTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
$connectedUidsOfDefaultLanguageRecord = $this->resolveConnectedRecordUids(
$result['processedTca']['columns'][$fieldName]['config'],
$result['tableName'],
$result['databaseRow'],
$result['databaseRow'][$fieldName]
);
$result['databaseRow'][$fieldName] = implode(',', $connectedUidsOfDefaultLanguageRecord);
$connectedUidsOfDefaultLanguageRecord = $this->getSubstitutedWorkspacedUids($result, $connectedUidsOfDefaultLanguageRecord, $childTableName);
if ($result['inlineCompileExistingChildren']) {
foreach ($connectedUidsOfDefaultLanguageRecord as $uid) {
try {
$compiledChild = $this->compileChild($result, $fieldName, $uid);
$result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
} catch (DatabaseRecordException $e) {
// Nothing to do here, missing child is just not being rendered.
}
}
}
return $result;
}
/**
* If there is a foreign_selector or foreign_unique configuration, fetch
* the list of possible records that can be connected and attach the to the
* inline configuration.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
*/
protected function addForeignSelectorAndUniquePossibleRecords(array $result, $fieldName)
{
if (!is_array($result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'] ?? null)) {
return $result;
}
$selectorOrUniqueConfiguration = $result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'];
$foreignFieldName = $selectorOrUniqueConfiguration['fieldName'];
$selectorOrUniquePossibleRecords = [];
if ($selectorOrUniqueConfiguration['config']['type'] === 'select') {
// Compile child table data for this field only
$selectDataInput = [
'request' => $result['request'],
'tableName' => $result['processedTca']['columns'][$fieldName]['config']['foreign_table'],
'command' => 'new',
// Since there is no existing record that may have a type, it does not make sense to
// do extra handling of pageTsConfig merged here. Just provide "parent" pageTS as is
'pageTsConfig' => $result['pageTsConfig'],
'userTsConfig' => $result['userTsConfig'],
'databaseRow' => $result['databaseRow'],
'processedTca' => [
'ctrl' => [],
'columns' => [
$foreignFieldName => [
'config' => $selectorOrUniqueConfiguration['config'],
],
],
],
'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
'site' => $result['site'],
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
];
$formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
$formDataGroup->setProviderList([TcaSelectItems::class]);
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class);
$compilerResult = $formDataCompiler->compile($selectDataInput, $formDataGroup);
$selectorOrUniquePossibleRecords = $compilerResult['processedTca']['columns'][$foreignFieldName]['config']['items'];
}
$result['processedTca']['columns'][$fieldName]['config']['selectorOrUniquePossibleRecords'] = $selectorOrUniquePossibleRecords;
return $result;
}
/**
* Compile a full child record
*
* @param array $result Result array of parent
* @param string $parentFieldName Name of parent field
* @param int $childUid Uid of child to compile
* @return array Full result array
*/
protected function compileChild(array $result, $parentFieldName, $childUid, $isInlineDefaultLanguageRecordInLocalizedParentContext = false)
{
$parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
$childTableName = $parentConfig['foreign_table'];
$inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($result['inlineStructure'], 0) ?: [];
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class);
$formDataCompilerInput = [
'request' => $result['request'],
'command' => 'edit',
'tableName' => $childTableName,
'vanillaUid' => (int)$childUid,
// Give incoming returnUrl down to children so they generate a returnUrl back to
// the originally opening record, also see "originalReturnUrl" in inline container
// and FormInlineAjaxController
'returnUrl' => $result['returnUrl'],
'isInlineChild' => true,
'isInlineDefaultLanguageRecordInLocalizedParentContext' => $isInlineDefaultLanguageRecordInLocalizedParentContext,
'inlineStructure' => $result['inlineStructure'],
'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
'inlineFirstPid' => $result['inlineFirstPid'],
'inlineParentConfig' => $parentConfig,
// values of the current parent element
// it is always a string either an id or new...
'inlineParentUid' => $result['databaseRow']['uid'],
'inlineParentTableName' => $result['tableName'],
'inlineParentFieldName' => $parentFieldName,
// values of the top most parent element set on first level and not overridden on following levels
'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: $inlineTopMostParent['uid'] ?? '',
'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: $inlineTopMostParent['table'] ?? '',
'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: $inlineTopMostParent['field'] ?? '',
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
];
// For foreign_selector with useCombination $mainChild is the mm record
// and $combinationChild is the child-child. For 1:n "normal" relations,
// $mainChild is just the normal child record and $combinationChild is empty.
$mainChild = $formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class));
if (($parentConfig['foreign_selector'] ?? false) && ($parentConfig['appearance']['useCombination'] ?? false)) {
try {
$mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig);
} catch (DatabaseRecordException $e) {
// The child could not be compiled, probably it was deleted and a dangling mm record
// exists. This is a data inconsistency, we catch this exception and create a flash message
$message = vsprintf(
$this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:formEngine.databaseRecordErrorInlineChildChild'),
[$e->getTableName(), $e->getUid(), $childTableName, (int)$childUid]
);
$flashMessage = new FlashMessage(
$message,
'',
ContextualFeedbackSeverity::ERROR
);
$this->flashMessageService->getMessageQueueByIdentifier()->enqueue($flashMessage);
}
}
return $mainChild;
}
/**
* With useCombination set, not only content of the intermediate table, but also
* the connected child should be rendered in one go. Prepare this here.
*
* @param array $child Full data array of "mm" record
* @param array $parentConfig TCA configuration of "parent"
* @return array Full data array of child
*/
protected function compileChildChild(array $child, array $parentConfig)
{
// foreign_selector on intermediate is type=select (resolved to array of uids)
// or type=group (resolved to array of associative arrays with 'table', 'uid', etc.)
$childChildValue = $child['databaseRow'][$parentConfig['foreign_selector']][0];
$childChildUid = is_array($childChildValue) ? $childChildValue['uid'] : $childChildValue;
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class);
$formDataCompilerInput = [
'request' => $child['request'],
'command' => 'edit',
'tableName' => $this->getChildChildTableName($parentConfig['foreign_selector'] ?? '', $child),
'vanillaUid' => (int)$childChildUid,
'isInlineChild' => true,
'isInlineChildExpanded' => $child['isInlineChildExpanded'],
// @todo: this is the wrong inline structure, isn't it? Shouldn't it contain the part from child child, too?
'inlineStructure' => $child['inlineStructure'],
'inlineFirstPid' => $child['inlineFirstPid'],
// values of the top most parent element set on first level and not overridden on following levels
'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $child['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $child['fullTca'],
];
$childChild = $formDataCompiler->compile($formDataCompilerInput, GeneralUtility::makeInstance(TcaDatabaseRecord::class));
return $childChild;
}
/**
* Substitute given list of uids in child table with workspace uid if needed
*
* @param array $result Result array
* @param array $connectedUids List of connected uids
* @param string $childTableName Name of child table
* @return int[] List of substituted uids
*/
protected function getSubstitutedWorkspacedUids(array $result, array $connectedUids, string $childTableName): array
{
$backendUser = $this->getBackendUser();
$newConnectedUids = [];
foreach ($connectedUids as $uid) {
// Fetch workspace version of a record (if any):
if ($backendUser->workspace !== 0
&& $result['tcaSchemata']->has($childTableName)
&& $result['tcaSchemata']->get($childTableName)->hasCapability(TcaSchemaCapability::Workspace)
) {
$workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($backendUser->workspace, $childTableName, $uid, 'uid,t3ver_state');
if (!empty($workspaceVersion)) {
$versionState = VersionState::tryFrom($workspaceVersion['t3ver_state'] ?? 0);
if ($versionState === VersionState::DELETE_PLACEHOLDER) {
continue;
}
$uid = $workspaceVersion['uid'];
}
}
$newConnectedUids[] = (int)$uid;
}
return $newConnectedUids;
}
/**
* Use RelationHandler to resolve connected uids.
*
* @param array $parentConfig TCA config section of parent
* @param string $parentTableName Name of parent table
* @param array $parentRecord Full parent record
* @param string $parentFieldValue Database value of parent record of this inline field
* @return array Array with connected uids
* @todo: Cover with unit tests
*/
protected function resolveConnectedRecordUids(array $parentConfig, string $parentTableName, array $parentRecord, string $parentFieldValue): array
{
$directlyConnectedIds = GeneralUtility::trimExplode(',', $parentFieldValue);
$parentUid = (int)$parentRecord['uid'];
// Relations to non-MM tables point to the LIVE version, so we need to ensure
// we use the live version that is sent to RelationHandler
if (empty($parentConfig['MM']) && (int)($parentRecord['t3ver_oid'] ?? 0) > 0) {
$parentUid = (int)$parentRecord['t3ver_oid'];
}
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
$relationHandler->start($parentFieldValue, $parentConfig['foreign_table'] ?? '', $parentConfig['MM'] ?? '', $parentUid, $parentTableName, $parentConfig);
$foreignRecordUids = $relationHandler->getValueArray();
$resolvedForeignRecordUids = [];
foreach ($foreignRecordUids as $aForeignRecordUid) {
if ($parentConfig['MM'] ?? $parentConfig['foreign_field'] ?? false) {
$resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
} else {
foreach ($directlyConnectedIds as $id) {
if ((int)$aForeignRecordUid === (int)$id) {
$resolvedForeignRecordUids[] = (int)$aForeignRecordUid;
}
}
}
}
return $resolvedForeignRecordUids;
}
/**
* The child-child table name is set in the child TCA "the selector field" and is depending on
* the TCA type (select or group) either the "foreign_table" or the (first) "allowed" table.
*/
protected function getChildChildTableName(string $foreignSelector, array $childConfiguration): string
{
$config = $childConfiguration['processedTca']['columns'][$foreignSelector]['config'] ?? [];
$type = $config['type'] ?? '';
return match ($type) {
'select' => $config['foreign_table'] ?? '',
'group' => GeneralUtility::trimExplode(',', $config['allowed'] ?? '', true)[0] ?? '',
default => '',
};
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,292 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Set or initialize configuration for inline fields in TCA
*/
class TcaInlineConfiguration implements FormDataProviderInterface
{
/**
* Find all inline fields and force proper configuration
*
* @return array
* @throws \UnexpectedValueException If inline configuration is broken
*/
public function addData(array $result)
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'inline') {
continue;
}
// Throw if an inline field without foreign_table is set
if (!isset($fieldConfig['config']['foreign_table'])) {
throw new \UnexpectedValueException(
'Inline field ' . $fieldName . ' of table ' . $result['tableName'] . ' must have a foreign_table config',
1443793404
);
}
$result = $this->initializeMinMaxItems($result, $fieldName);
$result = $this->initializeChildrenLanguage($result, $fieldName);
$result = $this->initializeAppearance($result, $fieldName);
$result = $this->addInlineSelectorAndUniqueConfiguration($result, $fieldName);
}
// If field is set to readOnly, set all fields of the relation to readOnly as well
if (isset($result['inlineParentConfig']) && isset($result['inlineParentConfig']['readOnly']) && $result['inlineParentConfig']['readOnly']) {
foreach ($result['processedTca']['columns'] as $columnName => $columnConfiguration) {
$result['processedTca']['columns'][$columnName]['config']['readOnly'] = true;
}
}
return $result;
}
/**
* Set and validate minitems and maxitems in config
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
* @return array
*/
protected function initializeMinMaxItems(array $result, $fieldName)
{
$config = $result['processedTca']['columns'][$fieldName]['config'];
$minItems = 0;
if (isset($config['minitems'])) {
$minItems = MathUtility::forceIntegerInRange($config['minitems'], 0);
}
$result['processedTca']['columns'][$fieldName]['config']['minitems'] = $minItems;
$maxItems = 99999;
if (isset($config['maxitems'])) {
$maxItems = MathUtility::forceIntegerInRange($config['maxitems'], 1);
}
$result['processedTca']['columns'][$fieldName]['config']['maxitems'] = $maxItems;
return $result;
}
/**
* Set appearance configuration
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
* @return array
*/
protected function initializeAppearance(array $result, $fieldName)
{
$config = $result['processedTca']['columns'][$fieldName]['config'];
if (!isset($config['appearance']) || !is_array($config['appearance'])) {
// Init appearance if not set
$config['appearance'] = [];
}
// Initialize position of the level links
if (!isset($config['appearance']['levelLinksPosition'])
|| !in_array($config['appearance']['levelLinksPosition'], ['top', 'bottom', 'both'], true)
) {
$config['appearance']['levelLinksPosition'] = 'top';
}
// Hide level links (no matter the defined position) for "use combination"
if (isset($config['foreign_selector']) && $config['foreign_selector']
&& (!isset($config['appearance']['useCombination']) || !$config['appearance']['useCombination'])
) {
$config['appearance']['showAllLocalizationLink'] = false;
$config['appearance']['showSynchronizationLink'] = false;
$config['appearance']['showNewRecordLink'] = false;
}
$config['appearance']['showPossibleLocalizationRecords']
= isset($config['appearance']['showPossibleLocalizationRecords']) && $config['appearance']['showPossibleLocalizationRecords'];
// Defines which controls should be shown in header of each record
$enabledControls = [
'info' => true,
'new' => true,
'dragdrop' => true,
'sort' => true,
'hide' => true,
'delete' => true,
'localize' => true,
];
if (isset($config['appearance']['enabledControls']) && is_array($config['appearance']['enabledControls'])) {
$config['appearance']['enabledControls'] = array_merge($enabledControls, $config['appearance']['enabledControls']);
} else {
$config['appearance']['enabledControls'] = $enabledControls;
}
$result['processedTca']['columns'][$fieldName]['config'] = $config;
return $result;
}
/**
* Set default value for child records 'sys_language_uid' field. This is relevant if a localized
* parent is edited and a child is added via the ajax call. The child should then have the same
* sys_language_uid as the parent.
* The method verifies if the parent is a localized parent, and writes the current languageField
* value into TCA ['config']['inline']['parentSysLanguageUid'] of the parent inline TCA field. The whole
* ['config'] section is transferred to the 'create new child' ajax controller, the value is then used within
* 'DatabaseRowInitializeNew' data provider to initialize the child languageField value with that value.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
*/
protected function initializeChildrenLanguage(array $result, $fieldName)
{
$childTableName = $result['processedTca']['columns'][$fieldName]['config']['foreign_table'];
if (empty($result['processedTca']['ctrl']['languageField'])
|| !$result['tcaSchemata']->get($childTableName)->isLanguageAware()
) {
return $result;
}
$parentConfig = $result['processedTca']['columns'][$fieldName]['config'];
$parentLanguageField = $result['processedTca']['ctrl']['languageField'];
if (!isset($parentConfig['inline']['parentSysLanguageUid'])
&& isset($result['databaseRow'][$parentLanguageField])
) {
if (is_array($result['databaseRow'][$parentLanguageField])) {
$result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid']
= (int)($result['databaseRow'][$parentLanguageField][0] ?? 0);
} else {
$result['processedTca']['columns'][$fieldName]['config']['inline']['parentSysLanguageUid']
= (int)($result['databaseRow'][$parentLanguageField] ?? 0);
}
}
return $result;
}
/**
* If foreign_selector or foreign_unique is set, this points to a field configuration of the child
* table. The InlineControlContainer may render a drop down field or an element browser later from this.
*
* Fetch configuration from child table configuration, sanitize and merge with
* overrideChildTca of foreign_selector if given that allows overriding this field definition again.
*
* Final configuration is written to selectorOrUniqueConfiguration of inline config section.
*
* @param array $result Result array
* @param string $fieldName Current handle field name
* @return array Modified item array
* @throws \UnexpectedValueException If configuration is broken
*/
protected function addInlineSelectorAndUniqueConfiguration(array $result, $fieldName)
{
$config = $result['processedTca']['columns'][$fieldName]['config'];
// Early return if neither foreign_unique nor foreign_selector are set
if (!isset($config['foreign_unique']) && !isset($config['foreign_selector'])) {
return $result;
}
// If both are set, they must point to the same field
if (isset($config['foreign_unique']) && isset($config['foreign_selector'])
&& $config['foreign_unique'] !== $config['foreign_selector']
) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ': If both foreign_unique and'
. ' foreign_selector are set, they must point to the same field',
1444995464
);
}
if (isset($config['foreign_unique'])) {
$fieldNameInChildConfiguration = $config['foreign_unique'];
} else {
$fieldNameInChildConfiguration = $config['foreign_selector'];
}
// Throw if field name in globals does not exist or is not of type select or group
$foreignTableSchema = $result['tcaSchemata']->has($config['foreign_table']) ? $result['tcaSchemata']->get($config['foreign_table']) : null;
if ($foreignTableSchema === null
|| !$foreignTableSchema->hasField($fieldNameInChildConfiguration)
|| ($foreignTableSchema->getField($fieldNameInChildConfiguration)->getType() !== 'select'
&& $foreignTableSchema->getField($fieldNameInChildConfiguration)->getType() !== 'group')
) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points in foreign_selector or foreign_unique'
. ' to field ' . $fieldNameInChildConfiguration . ' of table ' . $config['foreign_table'] . ', but this field'
. ' is either not defined or is not of type select or group',
1444996537
);
}
$selectorOrUniqueConfiguration = [
'config' => $foreignTableSchema->getField($fieldNameInChildConfiguration)->getConfiguration(),
];
// Merge overrideChildTca of foreign_selector if given
if (isset($config['foreign_selector'], $config['overrideChildTca']['columns'][$config['foreign_selector']]['config'])
&& is_array($config['overrideChildTca']['columns'][$config['foreign_selector']]['config'])
) {
$selectorOrUniqueConfiguration['config'] = array_replace_recursive($selectorOrUniqueConfiguration['config'], $config['overrideChildTca']['columns'][$config['foreign_selector']]['config']);
}
// Add field name to config for easy access later
$selectorOrUniqueConfiguration['fieldName'] = $fieldNameInChildConfiguration;
// Add remote table name for easy access later
if ($selectorOrUniqueConfiguration['config']['type'] === 'select') {
if (!isset($selectorOrUniqueConfiguration['config']['foreign_table'])) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points in foreign_selector or foreign_unique'
. ' to field ' . $fieldNameInChildConfiguration . ' of table ' . $config['foreign_table'] . '. This field'
. ' is of type select and must define foreign_table',
1445078627
);
}
$foreignTable = $selectorOrUniqueConfiguration['config']['foreign_table'];
} else {
if (!isset($selectorOrUniqueConfiguration['config']['allowed'])) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points in foreign_selector or foreign_unique'
. ' to field ' . $fieldNameInChildConfiguration . ' of table ' . $config['foreign_table'] . '. This field'
. ' is of type select and must define allowed',
1445078628
);
}
$foreignTable = $selectorOrUniqueConfiguration['config']['allowed'];
}
$selectorOrUniqueConfiguration['foreignTable'] = $foreignTable;
// If this is a foreign_selector field, mark it as such for data fetching later
$selectorOrUniqueConfiguration['isSelector'] = false;
if (isset($config['foreign_selector'])) {
$selectorOrUniqueConfiguration['isSelector'] = true;
}
// If this is a foreign_unique field, mark it a such for unique data fetching later
$selectorOrUniqueConfiguration['isUnique'] = false;
if (isset($config['foreign_unique'])) {
$selectorOrUniqueConfiguration['isUnique'] = true;
}
// Add field configuration to inline configuration
$result['processedTca']['columns'][$fieldName]['config']['selectorOrUniqueConfiguration'] = $selectorOrUniqueConfiguration;
return $result;
}
}
@@ -0,0 +1,87 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* Fetch information of user specific inline record expanded / collapsed state
* from user->uc and put it into $result['inlineExpandCollapseStateArray']
*/
class TcaInlineExpandCollapseState implements FormDataProviderInterface
{
/**
* Add inline expand / collapse state
*
* @return array
*/
public function addData(array $result)
{
if (empty($result['inlineExpandCollapseStateArray'])) {
$fullInlineState = json_decode($this->getBackendUser()->uc['inlineView'] ?? '', true);
if (!is_array($fullInlineState)) {
$fullInlineState = [];
}
$inlineStateForTable = [];
if (!empty($result['inlineTopMostParentUid']) && !empty($result['inlineTopMostParentTableName'])) {
// Happens in inline ajax context, top parent uid and top parent table are set
if ($result['command'] !== 'new') {
$table = $result['inlineTopMostParentTableName'];
$uid = $result['inlineTopMostParentUid'];
if (!empty($fullInlineState[$table][$uid])) {
$inlineStateForTable = $fullInlineState[$table][$uid];
}
}
} else {
// Default case for a single record
if ($result['command'] !== 'new') {
$table = $result['tableName'];
$uid = $result['databaseRow']['uid'] ?? 0;
if (!empty($fullInlineState[$table][$uid])) {
$inlineStateForTable = $fullInlineState[$table][$uid];
}
}
}
$result['inlineExpandCollapseStateArray'] = $inlineStateForTable;
}
if (!$result['isInlineChildExpanded']) {
// If the record is an inline child that is not expanded, it is not necessary to calculate all fields
$isExistingRecord = $result['command'] === 'edit';
$inlineConfig = $result['inlineParentConfig'];
$collapseAll = isset($inlineConfig['appearance']['collapseAll']) && $inlineConfig['appearance']['collapseAll'];
$expandAll = isset($inlineConfig['appearance']['collapseAll']) && !$inlineConfig['appearance']['collapseAll'];
$expandCollapseStateArray = $result['inlineExpandCollapseStateArray'];
$foreignTable = $result['inlineParentConfig']['foreign_table'] ?? null;
$isExpandedByUcState = $foreignTable !== null
&& isset($expandCollapseStateArray[$foreignTable])
&& is_array($expandCollapseStateArray[$foreignTable])
&& in_array($result['databaseRow']['uid'], $expandCollapseStateArray[$foreignTable]) !== false;
if (!$isExistingRecord || ($isExpandedByUcState && !$collapseAll) || $expandAll || $result['isInlineAjaxOpeningContext']) {
$result['isInlineChildExpanded'] = true;
}
}
return $result;
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,50 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Determine whether the child is on symmetric side or not.
*
* TCA ctrl fields like label and label_alt are evaluated and their
* current values from databaseRow used to create the title.
*/
class TcaInlineIsOnSymmetricSide implements FormDataProviderInterface
{
/**
* Enrich the processed record information with the resolved title
*
* @param array $result Incoming result array
* @return array Modified array
*/
public function addData(array $result)
{
if (!$result['isInlineChild']) {
return $result;
}
$result['isOnSymmetricSide'] = MathUtility::canBeInterpretedAsInteger($result['databaseRow']['uid'])
&& ($result['inlineParentConfig']['symmetric_field'] ?? false)
// non-strict comparison by intention
&& ($result['inlineParentUid'] == $result['databaseRow'][$result['inlineParentConfig']['symmetric_field']][0]
|| (is_array($result['databaseRow'][$result['inlineParentConfig']['symmetric_field']][0])
&& $result['inlineParentUid'] == $result['databaseRow'][$result['inlineParentConfig']['symmetric_field']][0]['uid']));
return $result;
}
}
@@ -0,0 +1,291 @@
<?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 TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaInputPlaceholderRecord;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Resolve placeholders for fields of type input or text. The placeholder value
* in the processedTca section of the result will be replaced with the resolved
* value.
*/
readonly class TcaInputPlaceholders implements FormDataProviderInterface
{
public function __construct(
private ConnectionPool $connectionPool,
) {}
/**
* Resolve placeholders for input/email/text fields. Placeholders that are simple
* strings will be returned unmodified. Placeholders beginning with __row are
* being resolved, possibly traversing multiple tables.
*
* @return array
*/
public function addData(array $result)
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
// Placeholders are only valid for input-like and text-like fields.
if (!isset($fieldConfig['config']['placeholder'], $fieldConfig['config']['type'])
|| (
$fieldConfig['config']['type'] !== 'input'
&& $fieldConfig['config']['type'] !== 'text'
&& $fieldConfig['config']['type'] !== 'number'
&& $fieldConfig['config']['type'] !== 'email'
&& $fieldConfig['config']['type'] !== 'link'
&& $fieldConfig['config']['type'] !== 'password'
&& $fieldConfig['config']['type'] !== 'datetime'
&& $fieldConfig['config']['type'] !== 'color'
&& $fieldConfig['config']['type'] !== 'json'
)
) {
continue;
}
// Resolve __row|field type placeholders
if (str_starts_with((string)$fieldConfig['config']['placeholder'], '__row|')) {
// split field names into array and remove the __row indicator
$fieldNameArray = array_slice(
GeneralUtility::trimExplode('|', $fieldConfig['config']['placeholder'], true),
1
);
$result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getPlaceholderValue($fieldNameArray, $result);
} elseif (!empty($fieldConfig['config']['placeholder'])) {
// Resolve placeholders from language files
$result['processedTca']['columns'][$fieldName]['config']['placeholder'] = $this->getLanguageService()->sL($fieldConfig['config']['placeholder']);
}
// Remove empty placeholders
if (empty($result['processedTca']['columns'][$fieldName]['config']['placeholder'])) {
unset($result['processedTca']['columns'][$fieldName]['config']['placeholder']);
}
}
return $result;
}
/**
* Recursively resolve the placeholder value. A placeholder string with a
* syntax of __row|field1|field2|field3 will be recursively resolved to a
* final value.
*
* @param array $fieldNameArray
* @param array $result
* @param int $recursionLevel
* @return string
*/
protected function getPlaceholderValue($fieldNameArray, $result, $recursionLevel = 0)
{
if ($recursionLevel > 99) {
// This should not happen, treat as misconfiguration
return '';
}
$fieldName = array_shift($fieldNameArray);
// Skip if a defined field was actually not present in the database row
// Using array_key_exists here, since NULL values are valid as well.
if (!array_key_exists($fieldName, $result['databaseRow'])) {
return '';
}
$value = $result['databaseRow'][$fieldName];
if (!isset($result['processedTca']['columns'][$fieldName]['config'])
|| !is_array($result['processedTca']['columns'][$fieldName]['config'])
) {
return (string)$value;
}
$fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
switch ($fieldConfig['type']) {
case 'select':
case 'category':
// The FormDataProviders already resolved the select items to an array of uids,
// filter out empty values that occur when no related record has been selected.
$possibleUids = array_filter($value);
$foreignTableName = $fieldConfig['foreign_table'] ?? '';
break;
case 'group':
$possibleUids = $this->getRelatedGroupFieldUids($fieldConfig, $value);
$foreignTableName = $this->getAllowedTableForGroupField($fieldConfig);
break;
case 'inline':
case 'file':
$possibleUids = array_filter(GeneralUtility::trimExplode(',', $value, true));
$foreignTableName = $fieldConfig['foreign_table'];
break;
default:
$possibleUids = [];
$foreignTableName = '';
}
if (!empty($possibleUids) && !empty($fieldNameArray)) {
if (count($possibleUids) > 1
&& $result['tcaSchemata']->get($foreignTableName)->isLanguageAware()
&& isset($result['currentSysLanguage'])
) {
$possibleUids = $this->getPossibleUidsByCurrentSysLanguage($result, $possibleUids, $foreignTableName, $result['currentSysLanguage']);
}
$relatedFormData = $this->getRelatedFormData($result, $foreignTableName, $possibleUids[0], $fieldNameArray[0]);
if ($result['tcaSchemata']->get($result['tableName'])->isLanguageAware()
&& isset($result['databaseRow'][$result['tcaSchemata']->get($result['tableName'])->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName()])
) {
$relatedFormData['currentSysLanguage'] = $result['databaseRow'][$result['tcaSchemata']->get($result['tableName'])->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName()];
}
$value = $this->getPlaceholderValue($fieldNameArray, $relatedFormData, $recursionLevel + 1);
}
if ($recursionLevel === 0 && is_array($value)) {
$value = implode(', ', $value);
}
return (string)$value;
}
/**
* Compile a formdata result set based on the tablename and record uid.
*
* @param string $tableName Name of the table for which to compile formdata
* @param int $uid UID of the record for which to compile the formdata
* @param string $columnToProcess The column that is required from the record
* @return array The compiled formdata
*/
protected function getRelatedFormData(array $result, $tableName, $uid, $columnToProcess)
{
$fakeDataInput = [
'request' => $result['request'],
'command' => 'edit',
'vanillaUid' => (int)$uid,
'tableName' => $tableName,
'inlineCompileExistingChildren' => false,
'columnsToProcess' => [$columnToProcess],
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
];
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class);
return $formDataCompiler->compile($fakeDataInput, GeneralUtility::makeInstance(TcaInputPlaceholderRecord::class));
}
/**
* Return uids of related records for group type fields. Uids consisting of
* multiple parts like [table]_[uid]|[title] will be reduced to integers and
* validated against the allowed table. Uids without a table prefix are
* accepted in any case.
*
* @param array $fieldConfig TCA "config" section for the group type field.
* @param array $value Related group field values prepared by TcaGroup data provider
*/
protected function getRelatedGroupFieldUids(array $fieldConfig, $value): array
{
$relatedUids = [];
$allowedTable = $this->getAllowedTableForGroupField($fieldConfig);
// Skip if it's not a resolvable foreign table
if (!$allowedTable) {
return [];
}
// Related group values have been prepared by TcaGroup data provider, an array is expected here
foreach ($value as $singleValue) {
$relatedUids[] = $singleValue['uid'];
}
return $relatedUids;
}
/**
* Will read the "allowed" value from the given field configuration
* and returns FALSE if none or more than one has been defined.
* Otherwise the name of the allowed table will be returned.
*
* @param array $fieldConfig TCA "config" section for the group type field.
* @return bool|string
*/
protected function getAllowedTableForGroupField(array $fieldConfig)
{
$allowedTable = false;
$allowedTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed'], true);
if (count($allowedTables) === 1) {
$allowedTable = $allowedTables[0];
}
return $allowedTable;
}
/**
* E.g. sys_file is not translatable, thus the uid of the translation of it's metadata has to be retrieved here.
*
* Get the uid of e.g. a file metadata entry for a given sys_language_uid and the possible translated data.
* If there is no translation available, return the uid of default language.
* If there is no value at all, return the "possible uids".
*
* @param array $result
* @param array $possibleUids
* @param string $foreignTableName
* @param int $currentLanguage
* @return array
*/
protected function getPossibleUidsByCurrentSysLanguage(array $result, array $possibleUids, $foreignTableName, $currentLanguage)
{
$languageField = $result['tcaSchemata']->get($foreignTableName)->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
$queryBuilder = $this->connectionPool->getQueryBuilderForTable($foreignTableName);
$possibleRecords = $queryBuilder->select('uid', $languageField)
->from($foreignTableName)
->where(
$queryBuilder->expr()->in(
'uid',
$queryBuilder->createNamedParameter($possibleUids, Connection::PARAM_INT_ARRAY)
),
$queryBuilder->expr()->in(
$languageField,
$queryBuilder->createNamedParameter([$currentLanguage, 0], Connection::PARAM_INT_ARRAY)
)
)
->groupBy($languageField, 'uid')
->executeQuery()
->fetchAllAssociative();
if (!empty($possibleRecords)) {
// Either only one record or first record matches language
if (count($possibleRecords) === 1
|| (int)$possibleRecords[0][$languageField] === (int)$currentLanguage
) {
return [$possibleRecords[0]['uid']];
}
// Language of second record matches language
return [$possibleRecords[1]['uid']];
}
return $possibleUids;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Resolve and prepare json data.
*/
class TcaJson extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
public function addData(array $result): array
{
// Currently only new records are considered
if ($result['command'] !== 'new') {
return $result;
}
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (($fieldConfig['config']['type'] ?? '') !== 'json') {
continue;
}
// Ensure that even for new records, the field is always an array - especially if a default value is defined
if (is_string($result['databaseRow'][$fieldName])) {
try {
$result['databaseRow'][$fieldName] = json_decode($result['databaseRow'][$fieldName], true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
$result['databaseRow'][$fieldName] = [];
}
}
}
return $result;
}
}
@@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\SiteFinder;
/**
* Resolve select items for the type="language" and set processed item list in processedTca
*/
class TcaLanguage extends AbstractItemProvider implements FormDataProviderInterface
{
public function __construct(
private readonly SiteFinder $siteFinder,
) {}
/**
* Fetch languages to add them as select item
*
* @throws \UnexpectedValueException
*/
public function addData(array $result): array
{
$table = $result['tableName'];
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (!isset($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'language') {
continue;
}
// Save user defined items and reset the field config items array afterwards
$userDefinedItems = $this->sanitizeItemArray($fieldConfig['config']['items'] ?? [], $table, $fieldName);
$fieldConfig['config']['items'] = [];
// Initialize site languages to be fetched
$siteLanguages = [];
if (($result['effectivePid'] ?? 0) === 0) {
// In case we deal with a pid=0 record or a record on a page outside
// of a site config, all languages from all sites should be added.
foreach ($this->siteFinder->getAllSites() as $site) {
// Add ALL languages from ALL sites
foreach ($site->getAllLanguages() as $languageId => $language) {
if (isset($siteLanguages[$languageId])) {
// Language already provided by another site, just add the label separately
$siteLanguages[$languageId]['title'] .= ', ' . $language->getTitle() . ' [Site: ' . $site->getIdentifier() . ']';
} else {
$siteLanguages[$languageId] = [
'title' => $language->getTitle() . ' [Site: ' . $site->getIdentifier() . ']',
'flagIconIdentifier' => $language->getFlagIdentifier(),
];
}
}
}
ksort($siteLanguages);
} elseif (($result['systemLanguageRows'] ?? []) !== []) {
$isLanguageField = $fieldName === ($result['processedTca']['ctrl']['languageField'] ?? '');
$currentLanguageId = (int)($result['databaseRow'][$fieldName] ?? 0);
$availablePageLanguageIds = [];
if ($isLanguageField && $table !== 'pages' && !empty($result['pageLanguageOverlayRows'])) {
foreach ($result['pageLanguageOverlayRows'] as $pageTranslation) {
$availablePageLanguageIds[] = (int)($pageTranslation['language_tag'] ?? 0);
}
}
// Add system languages available for the current site
foreach ($result['systemLanguageRows'] as $languageId => $language) {
if ($languageId === -1) {
continue;
}
if ($isLanguageField && $table === 'pages') {
// For pages table language field: only show the current language
// (language cannot be changed via FormEngine after creation)
if ($languageId === $currentLanguageId) {
$siteLanguages[$languageId] = [
'title' => $language['title'],
'flagIconIdentifier' => $language['flagIconIdentifier'],
];
}
} elseif ($isLanguageField && $availablePageLanguageIds !== []) {
// For other tables' language field: only show languages with page translations
if ($languageId === 0 || in_array($languageId, $availablePageLanguageIds, true)) {
$siteLanguages[$languageId] = [
'title' => $language['title'],
'flagIconIdentifier' => $language['flagIconIdentifier'],
];
}
} else {
$siteLanguages[$languageId] = [
'title' => $language['title'],
'flagIconIdentifier' => $language['flagIconIdentifier'],
];
}
}
}
if ($siteLanguages !== []) {
// In case siteLanguages are available, add the "site languages" group
$fieldConfig['config']['items'] = [
[
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.siteLanguages',
'value' => '--div--',
],
];
// Add the fetched site languages to the field config items array
foreach ($siteLanguages as $languageId => $language) {
$fieldConfig['config']['items'][] = [
'label' => $language['title'],
'value' => $languageId,
'icon' => $language['flagIconIdentifier'],
];
}
}
// Add the "special" group for "ALL" and / or user defined items
if (($table !== 'pages' && isset($result['systemLanguageRows'][-1])) || $userDefinedItems !== []) {
$fieldConfig['config']['items'][] = [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.specialLanguages',
'value' => '--div--',
];
}
// Add "-1" for all TCA records except pages in case the user is allowed to.
// The item is added to the "special" group, in order to not provide it as default by accident.
if ($table !== 'pages' && isset($result['systemLanguageRows'][-1])) {
$fieldConfig['config']['items'][] = [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.allLanguages',
'value' => -1,
'icon' => 'flags-multiple',
];
}
// Add user defined items again so they are in the "special" group
$fieldConfig['config']['items'] = array_merge($fieldConfig['config']['items'], $userDefinedItems);
// Respect TSconfig options
$fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->addItemsFromPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
// In case no items are set at this point, we can write this back and continue with the next column
if ($fieldConfig['config']['items'] === []) {
$result['processedTca']['columns'][$fieldName] = $fieldConfig;
continue;
}
// Check current database value
$currentDatabaseValue = (int)($result['databaseRow'][$fieldName] ?? 0);
if (!in_array($currentDatabaseValue, array_map(intval(...), array_column($fieldConfig['config']['items'], 'value')), true)) {
// Current value is invalid, so add it with a proper message at the top
$fieldConfig['config']['items'] = $this->addInvalidItem($result, $table, $fieldName, $currentDatabaseValue, $fieldConfig['config']['items']);
}
// Reinitialize array keys
$fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']);
// In case the last element is a divider, remove it
if ((string)($fieldConfig['config']['items'][array_key_last($fieldConfig['config']['items'])]['value'] ?? '') === '--div--') {
array_pop($fieldConfig['config']['items']);
}
// Translate labels
$fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName);
// Add icons
$fieldConfig['config']['items'] = $this->addIconFromAltIcons($result, $fieldConfig['config']['items'], $table, $fieldName);
$result['processedTca']['columns'][$fieldName] = $fieldConfig;
}
return $result;
}
protected function addInvalidItem(
array $result,
string $table,
string $fieldName,
int $invalidValue,
array $items
): array {
// Early return if there are no items or invalid values should not be displayed
if (($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['disableNoMatchingValueElement'] ?? false)
|| ($result['processedTca']['columns'][$fieldName]['config']['disableNoMatchingValueElement'] ?? false)
) {
return $items;
}
$noMatchingLabel = isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label'])
? $this->getLanguageService()->sL(trim($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label']))
: '[ ' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue') . ' ]';
// Add the invalid value at the top
array_unshift($items, ['label' => @sprintf($noMatchingLabel, $invalidValue), 'value' => $invalidValue, 'icon' => null]);
return $items;
}
}
@@ -0,0 +1,111 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
/**
* Resolve radio items and set processed item list in processedTca
*/
class TcaRadioItems extends AbstractItemProvider implements FormDataProviderInterface
{
/**
* Resolve radio items
*
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
$languageService = $this->getLanguageService();
$table = $result['tableName'];
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'radio') {
continue;
}
if (!isset($fieldConfig['config']['items']) || !is_array($fieldConfig['config']['items'])) {
throw new \UnexpectedValueException(
'Radio field ' . $fieldName . ' of TCA table ' . $result['tableName'] . ' must have \'config\' \'items\' definition',
1438594829
);
}
$config = $fieldConfig['config'];
$items = $config['items'];
// Sanitize items and translate labels
$newItems = [];
foreach ($items as $itemKey => $itemValue) {
if (!is_array($itemValue)) {
throw new \UnexpectedValueException(
'Item ' . $itemKey . ' of field ' . $fieldName . ' of TCA table ' . $result['tableName'] . ' is not an array as expected',
1438607163
);
}
if (!array_key_exists('label', $itemValue)) {
throw new \UnexpectedValueException(
'Item ' . $itemKey . ' of field ' . $fieldName . ' of TCA table ' . $result['tableName'] . ' has no label',
1438607164
);
}
if (!array_key_exists('value', $itemValue)) {
throw new \UnexpectedValueException(
'Item ' . $itemKey . ' of field ' . $fieldName . ' of TCA table ' . $result['tableName'] . ' has no value',
1438607165
);
}
$newItems[$itemKey] = [
'label' => $languageService->sL(trim($itemValue['label'])),
'value' => $itemValue['value'],
];
}
$items = $newItems;
// Resolve "itemsProcFunc"
if (!empty($config['itemsProcFunc']) || !empty($config['itemsProcessors'])) {
$items = $this->resolveItemsProcessorFunction($result, $fieldName, $items);
// itemsProcFunc must not be used anymore
unset(
$result['processedTca']['columns'][$fieldName]['config']['itemsProcFunc'],
$result['processedTca']['columns'][$fieldName]['config']['itemsProcessors']
);
}
// Set label overrides from page TSconfig if given
if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'])
&& is_array($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'])
) {
foreach ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['altLabels.'] as $itemKey => $label) {
if (isset($items[$itemKey]['label'])) {
$items[$itemKey]['label'] = $languageService->sL(trim($label));
}
}
}
$result['processedTca']['columns'][$fieldName]['config']['items'] = $items;
}
return $result;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,421 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Domain\DateTimeFactory;
use TYPO3\CMS\Core\Localization\DateFormatter;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Determine the title of a record and write it to $result['recordTitle'].
*
* TCA ctrl fields like label and label_alt are evaluated and their
* current values from databaseRow used to create the title.
*/
class TcaRecordTitle implements FormDataProviderInterface
{
/**
* Enrich the processed record information with the resolved title
*
* @param array $result Incoming result array
* @return array Modified array
*/
public function addData(array $result)
{
if (!isset($result['processedTca']['ctrl']['label'])) {
throw new \UnexpectedValueException(
'TCA of table ' . $result['tableName'] . ' misses required [\'ctrl\'][\'label\'] definition.',
1443706103
);
}
if ($result['isInlineChild'] && isset($result['processedTca']['ctrl']['formattedLabel_userFunc'])) {
// inline child with formatted user func is first
$parameters = [
'table' => $result['tableName'],
'row' => $result['databaseRow'],
'title' => '',
'isOnSymmetricSide' => $result['isOnSymmetricSide'],
'options' => $result['processedTca']['ctrl']['formattedLabel_userFunc_options'] ?? [],
'parent' => [
'uid' => $result['databaseRow']['uid'],
'config' => $result['inlineParentConfig'],
],
];
// callUserFunction requires a third parameter, but we don't want to give $this as reference!
$null = null;
GeneralUtility::callUserFunction($result['processedTca']['ctrl']['formattedLabel_userFunc'], $parameters, $null);
$result['recordTitle'] = $parameters['title'];
} elseif ($result['isInlineChild'] && (isset($result['inlineParentConfig']['foreign_label'])
|| isset($result['inlineParentConfig']['symmetric_label']))
) {
// inline child with foreign label or symmetric inline child with symmetric_label
$fieldName = $result['isOnSymmetricSide']
? $result['inlineParentConfig']['symmetric_label']
: $result['inlineParentConfig']['foreign_label'];
$result['recordTitle'] = $this->getRecordTitleForField($fieldName, $result);
} elseif (isset($result['processedTca']['ctrl']['label_userFunc'])) {
// userFunc takes precedence over everything else
$parameters = [
'table' => $result['tableName'],
'row' => $result['databaseRow'],
'title' => '',
'options' => $result['processedTca']['ctrl']['label_userFunc_options'] ?? [],
];
$null = null;
GeneralUtility::callUserFunction($result['processedTca']['ctrl']['label_userFunc'], $parameters, $null);
$result['recordTitle'] = $parameters['title'];
} else {
// standard record
$result = $this->getRecordTitleByLabelProperties($result);
}
return $result;
}
/**
* Build the record title from label, label_alt and label_alt_force properties
*
* @param array $result Incoming result array
* @return array Modified result array
*/
protected function getRecordTitleByLabelProperties(array $result)
{
$titles = [];
$titleByLabel = $this->getRecordTitleForField($result['processedTca']['ctrl']['label'], $result);
if (!empty($titleByLabel)) {
$titles[] = $titleByLabel;
}
$labelAltForce = isset($result['processedTca']['ctrl']['label_alt_force'])
? (bool)$result['processedTca']['ctrl']['label_alt_force']
: false;
if (!empty($result['processedTca']['ctrl']['label_alt']) && ($labelAltForce || empty($titleByLabel))) {
// Dive into label_alt evaluation if label_alt_force is set or if label did not came up with a title yet
$labelAltFields = GeneralUtility::trimExplode(',', $result['processedTca']['ctrl']['label_alt'], true);
foreach ($labelAltFields as $fieldName) {
$titleByLabelAlt = $this->getRecordTitleForField($fieldName, $result);
if (!empty($titleByLabelAlt)) {
$titles[] = $titleByLabelAlt;
}
if (!$labelAltForce && !empty($titleByLabelAlt)) {
// label_alt_force creates a comma separated list of multiple fields.
// If not set, one found field with content is enough
break;
}
}
}
$result['recordTitle'] = implode(', ', $titles);
return $result;
}
/**
* Record title of a single field
*
* @param string $fieldName Field to handle
* @param array $result Incoming result array
* @return string
*/
protected function getRecordTitleForField($fieldName, $result)
{
if ($fieldName === 'uid') {
// uid return field content directly since it usually has not TCA definition
return $result['databaseRow']['uid'];
}
if (!isset($result['processedTca']['columns'][$fieldName]['config']['type'])
|| !is_string($result['processedTca']['columns'][$fieldName]['config']['type'])
) {
return '';
}
$recordTitle = '';
$rawValue = null;
if (array_key_exists($fieldName, $result['databaseRow'])) {
$rawValue = $result['databaseRow'][$fieldName];
}
$fieldConfig = $result['processedTca']['columns'][$fieldName]['config'];
switch ($fieldConfig['type']) {
case 'radio':
$recordTitle = $this->getRecordTitleForRadioType($rawValue, $fieldConfig);
break;
case 'inline':
case 'file':
$recordTitle = $this->getRecordTitleForInlineType(
$rawValue,
$result['processedTca']['columns'][$fieldName]['children'] ?? []
);
break;
case 'select':
case 'category':
$recordTitle = $this->getRecordTitleForSelectType($rawValue, $fieldConfig);
break;
case 'group':
$recordTitle = $this->getRecordTitleForGroupType($rawValue);
break;
case 'folder':
$recordTitle = $this->getRecordTitleForFolderType($rawValue);
break;
case 'check':
$recordTitle = $this->getRecordTitleForCheckboxType($rawValue, $fieldConfig);
break;
case 'input':
case 'number':
case 'uuid':
$recordTitle = $rawValue ?? '';
break;
case 'country':
$recordTitle = $this->getRecordTitleForCountryType($rawValue, $result, $fieldName);
break;
case 'text':
case 'email':
case 'link':
case 'color':
$recordTitle = $this->getRecordTitleForStandardTextField($rawValue);
break;
case 'datetime':
$recordTitle = $this->getRecordTitleForDatetimeType($rawValue, $fieldConfig);
break;
case 'password':
$recordTitle = $this->getRecordTitleForPasswordType($rawValue);
break;
case 'flex':
// @todo: Check if and how a label could be generated from flex field data
break;
case 'json':
// @todo: Check if and how a label could be generated from json field data
default:
}
return $recordTitle;
}
/**
* Return the record title for radio fields
*
* @param mixed $value Current database value of this field
* @param array $fieldConfig TCA field configuration
* @return string
*/
protected function getRecordTitleForRadioType($value, $fieldConfig)
{
if (!isset($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
return '';
}
foreach ($fieldConfig['items'] as $item) {
if ((string)$value === (string)$item['value']) {
return $item['label'];
}
}
return '';
}
/**
* @param int $value
* @return string
*/
protected function getRecordTitleForInlineType($value, array $children)
{
foreach ($children as $child) {
if ((int)$value === $child['vanillaUid']) {
return $child['recordTitle'];
}
}
return '';
}
/**
* Return the record title for database records
*
* @param mixed $value Current database value of this field
* @param array $fieldConfig TCA field configuration
* @return string
*/
protected function getRecordTitleForSelectType($value, $fieldConfig)
{
if (!is_array($value)) {
return '';
}
$labelParts = [];
if (!empty($fieldConfig['items'])) {
$listOfValues = array_column($fieldConfig['items'], 'value');
foreach ($value as $itemValue) {
$itemKey = array_search($itemValue, $listOfValues);
if ($itemKey !== false) {
$labelParts[] = $fieldConfig['items'][$itemKey]['label'];
}
}
}
$title = implode(', ', $labelParts);
if (empty($title) && !empty($value)) {
$title = implode(', ', $value);
}
return $title;
}
/**
* Return the record title for database records of type "country"
*
* @param mixed $value Current database value of this field
* @param array $result Incoming result array
* @param string $fieldName Field to handle
* @return string
*/
protected function getRecordTitleForCountryType($value, $result, $fieldName)
{
// @todo - There probably is a better way to get all valid items
// for TcaCountry?!
$tcaCountry = GeneralUtility::makeInstance(TcaCountry::class);
$processedResult = $tcaCountry->addData($result);
$countries = $processedResult['processedTca']['columns'][$fieldName]['config']['items'] ?? [];
// Iterate all possible countries. Fetch the one that matches our $value.
// Note that the 'label' option already resolved to the proper
// possible keys (name, localizedName, officialName, localizedOfficialName, iso2, iso3)
// due to the specifications store in [config] within $result.
foreach ($countries as $country) {
if ($country['value'] === $value) {
return $country['label'];
}
}
// Fallback if no country was resolved.
// @todo - Should this better return an empty value instead?
return $value;
}
/**
* Return the record title for database records
*
* @param mixed $value Current database value of this field
* @return string
*/
protected function getRecordTitleForGroupType($value)
{
$labelParts = [];
foreach ($value as $singleValue) {
$labelParts[] = $singleValue['title'];
}
return implode(', ', $labelParts);
}
/**
* Return the folder names
*
* @param array $value Current database value of this field
*/
protected function getRecordTitleForFolderType(array $value): string
{
$labelParts = [];
foreach ($value as $singleValue) {
$labelParts[] = $singleValue['folder'];
}
return implode(', ', $labelParts);
}
/**
* Returns the record title for checkbox fields
*
* @param mixed $value Current database value of this field
* @param array $fieldConfig TCA field configuration
* @return string
*/
protected function getRecordTitleForCheckboxType($value, $fieldConfig)
{
$languageService = $this->getLanguageService();
if (empty($fieldConfig['items']) || !is_array($fieldConfig['items'])) {
$title = $value
? $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes')
: $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no');
} else {
$labelParts = [];
foreach ($fieldConfig['items'] as $key => $val) {
if ((int)$value & 2 ** $key) {
$labelParts[] = $val['label'];
}
}
$title = implode(', ', $labelParts);
}
return $title;
}
/**
* Returns the record title for not transformed text fields
*
* @param mixed $value Current database value of this field
*/
protected function getRecordTitleForStandardTextField(mixed $value): string
{
if (!is_string($value)) {
return '';
}
return trim(strip_tags($value));
}
protected function getRecordTitleForDatetimeType(?\DateTimeInterface $datetime, array $fieldConfig): string
{
if ($datetime === null) {
return '';
}
$format = DateTimeFactory::getFormatFromTCAConfig($fieldConfig);
if ($format === 'date') {
$ageSuffix = '';
// Generate age suffix as long as not explicitly suppressed
if (!($fieldConfig['disableAgeDisplay'] ?? false)) {
$now = DateTimeFactory::createFromTimestamp($GLOBALS['EXEC_TIME']);
$ageSuffix = sprintf(' (%s)', (new DateFormatter())->formatDateInterval(
$now->diff($datetime),
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
));
}
return BackendUtility::date($datetime->getTimestamp()) . $ageSuffix;
}
if ($format === 'time') {
return $datetime->format('H:i');
}
if ($format === 'timesec') {
return $datetime->format('H:i:s');
}
if ($format === 'datetime') {
return BackendUtility::datetime($datetime->getTimestamp());
}
if ($format === 'datetimesec') {
return BackendUtility::datetimesec($datetime->getTimestamp());
}
return '';
}
/**
* Returns the record title for password fields
*
* @param mixed $value Current database value of this field
*/
protected function getRecordTitleForPasswordType(mixed $value): string
{
return $value ? '********' : '';
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,235 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Form\Processor\SelectItemProcessor;
use TYPO3\CMS\Core\Configuration\Processor\Placeholder\EnvPlaceholderProcessor;
use TYPO3\CMS\Core\Schema\Struct\SelectItem;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Resolve select items, set processed item list in processedTca, sanitize and resolve database field
*/
class TcaSelectItems extends AbstractItemProvider implements FormDataProviderInterface
{
public function __construct(
private readonly SelectItemProcessor $selectItemProcessor,
private readonly EnvPlaceholderProcessor $envPlaceholderProcessor,
) {}
/**
* Resolve select items
*
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
$table = $result['tableName'];
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'select') {
continue;
}
// Make sure we are only processing supported renderTypes
if (!$this->isTargetRenderType($fieldConfig)) {
continue;
}
$fieldConfig['config']['items'] = $this->sanitizeItemArray($fieldConfig['config']['items'] ?? [], $table, $fieldName);
$fieldConfig['config']['maxitems'] = MathUtility::forceIntegerInRange($fieldConfig['config']['maxitems'] ?? 0, 0, 99999);
if ($fieldConfig['config']['maxitems'] === 0) {
$fieldConfig['config']['maxitems'] = 99999;
}
$fieldConfig['config']['items'] = $this->addItemsFromFolder($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->addItemsFromForeignTable($result, $fieldName, $fieldConfig['config']['items']);
// Resolve "itemsProcFunc"
if (!empty($fieldConfig['config']['itemsProcFunc']) || !empty($fieldConfig['config']['itemsProcessors'])) {
$fieldConfig['config']['items'] = $this->resolveItemsProcessorFunction($result, $fieldName, $fieldConfig['config']['items']);
// itemsProcFunc must not be used anymore
unset(
$fieldConfig['config']['itemsProcFunc'],
$fieldConfig['config']['itemsProcessors']
);
}
// removing items before $dynamicItems and $removedItems have been built results in having them
// not populated to the dynamic database row and displayed as "invalid value" in the forms view
$fieldConfig['config']['items'] = $this->removeItemsByUserStorageRestriction($result, $fieldName, $fieldConfig['config']['items']);
$removedItems = $fieldConfig['config']['items'];
$fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->addItemsFromPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->removeItemsByUserAuthMode($result, $fieldName, $fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $fieldConfig['config']['items']);
$removedItems = array_diff_key($removedItems, $fieldConfig['config']['items']);
$currentDatabaseValuesArray = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName);
// Check if it's a new record to respect TCAdefaults
if (!empty($fieldConfig['config']['MM']) && $result['command'] !== 'new') {
// Getting the current database value on a mm relation doesn't make sense since the amount of selected
// relations is stored in the field and not the uids of the items
$currentDatabaseValuesArray = [];
}
$result['databaseRow'][$fieldName] = $currentDatabaseValuesArray;
// add item values as keys to determine which items are stored in the database and should be preselected
$itemArrayValues = array_column(
array_map(fn(SelectItem|array $item): array => $item instanceof SelectItem ? $item->toArray() : $item, $fieldConfig['config']['items']),
'value'
);
$itemArray = array_fill_keys(
$itemArrayValues,
$fieldConfig['config']['items']
);
$result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, $itemArray);
$fieldConfig['config']['items'] = $this->addInvalidItemsFromDatabase(
$result,
$table,
$fieldName,
$fieldConfig,
$currentDatabaseValuesArray,
$removedItems
);
// Translate labels and add icons
// skip file of sys_file_metadata which is not rendered anyway but can use all memory
if (!($table === 'sys_file_metadata' && $fieldName === 'file')) {
$fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName);
$fieldConfig['config']['items'] = $this->addIconFromAltIcons($result, $fieldConfig['config']['items'], $table, $fieldName);
}
$unresolvedValue = $result['databaseRow'][$fieldName][0] ?? null;
if ($table === 'site' && $this->envPlaceholderProcessor->canProcess($unresolvedValue ?? '')) {
$resolvedValue = $this->envPlaceholderProcessor->process($unresolvedValue);
$itemByResolvedPlaceholder = array_find(
$fieldConfig['config']['items'],
fn(array $v) => (string)$v['value'] === $resolvedValue
);
if ($itemByResolvedPlaceholder === null) {
throw new \RuntimeException(
sprintf('Invalid placeholder value "%s" for "%s"', $resolvedValue, $unresolvedValue),
1764310149
);
}
$fieldConfig['config']['items'][0] = [
...$itemByResolvedPlaceholder,
'label' => $itemByResolvedPlaceholder['label'],
'value' => $unresolvedValue,
];
}
// Keys may contain table names, so a numeric array is created
$fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']);
$fieldConfig['config']['items'] = $this->selectItemProcessor->groupAndSortItems(
$fieldConfig['config']['items'],
$fieldConfig['config']['itemGroups'] ?? [],
$fieldConfig['config']['sortItems'] ?? []
);
$result['processedTca']['columns'][$fieldName] = $fieldConfig;
}
return $result;
}
/**
* Add values that are currently listed in the database columns but not in the selectable items list
* back to the list.
*
* @param array $result The current result array.
* @param string $table The current table name
* @param string $fieldName The current field name
* @param array $fieldConf The configuration of the current field.
* @param array $databaseValues The item values from the database, can contain invalid items!
* @param array $removedItems Items removed by access checks and restrictions, must not be added as invalid values
* @return array
*/
public function addInvalidItemsFromDatabase(array $result, $table, $fieldName, array $fieldConf, array $databaseValues, array $removedItems)
{
// Early return if there are no items or invalid values should not be displayed
if (empty($fieldConf['config']['items'])
|| ($fieldConf['config']['renderType'] ?? '') !== 'selectSingle'
|| ($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['disableNoMatchingValueElement'] ?? false)
|| ($fieldConf['config']['disableNoMatchingValueElement'] ?? false)
) {
return $fieldConf['config']['items'];
}
$languageService = $this->getLanguageService();
$noMatchingLabel = isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label'])
? $languageService->sL(trim($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['noMatchingValue_label']))
: '[ ' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingLabel') . ' ]';
$unmatchedValues = array_diff(
array_values($databaseValues),
array_column(
array_map(
fn(SelectItem|array $item): array => $item instanceof SelectItem ? $item->toArray() : $item,
$fieldConf['config']['items']
),
'value'
),
array_column(
array_map(
fn(SelectItem|array $item): array => $item instanceof SelectItem ? $item->toArray() : $item,
$removedItems
),
'value'
)
);
foreach ($unmatchedValues as $unmatchedValue) {
$invalidItem = [
'label' => @sprintf($noMatchingLabel, $unmatchedValue),
'value' => $unmatchedValue,
'icon' => null,
'group' => 'none', // put it in the very first position in the "none" group
];
array_unshift($fieldConf['config']['items'], $invalidItem);
}
return $fieldConf['config']['items'];
}
/**
* Determines whether the current field is a valid target for this DataProvider
*
* @return bool
*/
protected function isTargetRenderType(array $fieldConfig)
{
return ($fieldConfig['config']['renderType'] ?? '') !== 'selectTree';
}
}
@@ -0,0 +1,223 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Tree\TableConfiguration\ArrayTreeRenderer;
use TYPO3\CMS\Core\Tree\TableConfiguration\TableConfigurationTree;
use TYPO3\CMS\Core\Tree\TableConfiguration\TreeDataProviderFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Data provider for type=select + renderType=selectTree fields.
*
* Used in combination with SelectTreeElement to create the base HTML for trees,
* does a little bit of sanitation and preparation then.
*
* Used in combination with FormSelectTreeAjaxController to fetch the final tree list, this is
* triggered if $result['selectTreeCompileItems'] is set to true. This way the tree item
* calculation is only triggered if needed in this ajax context. Writes the prepared
* item array to ['config']['items'] in this case.
*/
class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProviderInterface
{
public function __construct(private readonly IconFactory $iconFactory) {}
/**
* Sanitize config options and resolve select items if requested.
*
* @return array
* @throws \UnexpectedValueException
*/
public function addData(array $result)
{
$table = $result['tableName'];
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'select') {
continue;
}
// Make sure we are only processing supported renderTypes
if (!$this->isTargetRenderType($fieldConfig)) {
continue;
}
$fieldConfig['config']['maxitems'] = MathUtility::forceIntegerInRange($fieldConfig['config']['maxitems'] ?? 0, 0, 99999);
if ($fieldConfig['config']['maxitems'] === 0) {
$fieldConfig['config']['maxitems'] = 99999;
}
$fieldConfig = $this->parseStartingPointsFromSiteConfiguration($result, $fieldConfig);
// A couple of tree specific config parameters can be overwritten via page TS.
// Pick those that influence the data fetching and write them into the config
// given to the tree data provider. This is additionally used in SelectTreeElement, so always do that.
if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['config.']['treeConfig.'])) {
$pageTsConfig = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['config.']['treeConfig.'];
if (isset($pageTsConfig['startingPoints'])) {
$fieldConfig['config']['treeConfig']['startingPoints']
= implode(',', array_unique(GeneralUtility::intExplode(',', (string)$pageTsConfig['startingPoints'])));
}
if (isset($pageTsConfig['appearance.']['expandAll'])) {
$fieldConfig['config']['treeConfig']['appearance']['expandAll'] = (bool)$pageTsConfig['appearance.']['expandAll'];
}
if (isset($pageTsConfig['appearance.']['maxLevels'])) {
$fieldConfig['config']['treeConfig']['appearance']['maxLevels'] = (int)$pageTsConfig['appearance.']['maxLevels'];
}
if (isset($pageTsConfig['appearance.']['nonSelectableLevels'])) {
$fieldConfig['config']['treeConfig']['appearance']['nonSelectableLevels'] = $pageTsConfig['appearance.']['nonSelectableLevels'];
}
}
// Prepare the list of currently selected nodes using RelationHandler
// This is needed to ensure a correct value initialization before the actual tree is loaded
$result['databaseRow'][$fieldName] = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName);
$result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, []);
// Preserve original TCA static items before overwriting with resolved items
$originalTcaItems = $fieldConfig['config']['items'] ?? [];
// Always resolve the full item list (static + foreign_table + TSconfig) with filtering.
// This is needed by TcaColumnsRemoveEmptyRelations to determine if the field has any
// selectable items, and is reused below for tree building in the AJAX context.
$staticItems = $this->sanitizeItemArray($originalTcaItems, $table, $fieldName);
$staticItems = array_merge($staticItems, $this->addItemsFromPageTsConfig($result, $fieldName, []));
$staticItems = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $staticItems);
$staticItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $staticItems);
$dynamicItems = $this->addItemsFromForeignTable($result, $fieldName);
$dynamicItems = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $dynamicItems);
$dynamicItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $dynamicItems);
$dynamicItems = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $dynamicItems);
$dynamicItems = $this->removeItemsByUserAuthMode($result, $fieldName, $dynamicItems);
$dynamicItems = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $dynamicItems);
// Store flat items for downstream providers (will be overwritten with tree structure during AJAX)
$fieldConfig['config']['items'] = array_merge($staticItems, $dynamicItems);
if ($result['selectTreeCompileItems']) {
$finalItems = [];
// Prepare the list of "static" items if there are any.
// "static" and "dynamic" is separated since the tree code only copes with "real" existing foreign nodes,
// so this "static" stuff allows defining tree items that don't really exist in the tree.
$itemsFromTca = $this->sanitizeItemArray($originalTcaItems, $table, $fieldName);
// List of additional items defined by page ts config "addItems"
$itemsFromPageTsConfig = $this->addItemsFromPageTsConfig($result, $fieldName, []);
// Resolve pageTsConfig item icons to markup
$finalPageTsConfigItems = [];
foreach ($itemsFromPageTsConfig as $item) {
if ($item['icon'] !== null) {
$item['icon'] = $this->iconFactory->getIcon($item['icon'], IconSize::SMALL)->getMarkup('inline');
}
$finalPageTsConfigItems[] = $item;
}
if (!empty($itemsFromTca) || !empty($finalPageTsConfigItems)) {
// First apply "keepItems" to $itemsFromTca, this will restrict the tca item list to only
// those items that are defined in page ts "keepItems" if given
$itemsFromTca = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $itemsFromTca);
// Then, merge the items from page ts "addItems" into item list, since "addItems" should
// add additional items even if they are not in the "keepItems" list
$staticItems = array_merge($itemsFromTca, $finalPageTsConfigItems);
// Now apply page ts config "removeItems", so this is *after* addItems, so "removeItems" could
// possibly remove items again that were added via "addItems"
$staticItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $staticItems);
// Now, apply user and access right restrictions to this item list
$staticItems = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $staticItems);
$staticItems = $this->removeItemsByUserAuthMode($result, $fieldName, $staticItems);
$staticItems = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $staticItems);
// Call itemsProcFunc if given. Note this function does *not* see the "dynamic" list of items
if (!empty($fieldConfig['config']['itemsProcFunc']) || !empty($fieldConfig['config']['itemsProcessors'])) {
$staticItems = $this->resolveItemsProcessorFunction($result, $fieldName, $staticItems);
// itemsProcFunc must not be used anymore
unset(
$fieldConfig['config']['itemsProcFunc'],
$fieldConfig['config']['itemsProcessors']
);
}
// translate any labels
$staticItems = $this->translateLabels($result, $staticItems, $table, $fieldName);
// and add icons from the static list
$staticItems = $this->addIconFromAltIcons($result, $staticItems, $table, $fieldName);
// Now compile the target items using the same array structure as the "dynamic" list below
foreach ($staticItems as $item) {
if ($item['value'] === '--div--') {
// Skip divs that may occur here for whatever reason
continue;
}
$finalItems[] = [
'identifier' => $item['value'],
'name' => $item['label'],
'icon' => $item['icon'] ?? '',
'iconOverlay' => '',
'depth' => 0,
'hasChildren' => false,
'selectable' => true,
'checked' => in_array($item['value'], $result['databaseRow'][$fieldName]),
];
}
}
// Reuse the already-fetched dynamic items to build uid whitelist for tree
$uidListOfAllDynamicItems = [];
foreach ($dynamicItems as $item) {
if ((int)$item['value'] > 0) {
$uidListOfAllDynamicItems[] = (int)$item['value'];
}
}
// Now kick in this tree stuff
$treeDataProvider = TreeDataProviderFactory::getDataProvider(
$fieldConfig['config'],
$table,
$fieldName,
$result['databaseRow']
);
$treeDataProvider->setSelectedList(implode(',', $result['databaseRow'][$fieldName]));
// Basically the tree foo fetches all tree nodes again (aaargs), then verifies if
// a given rows uid is within this "list of allowed uids". It then creates an object
// tree representing the nested tree, just to collapse all that to a flat array again. Yay ...
$treeDataProvider->setItemWhiteList($uidListOfAllDynamicItems);
$treeDataProvider->initializeTreeData();
$treeRenderer = GeneralUtility::makeInstance(ArrayTreeRenderer::class);
$tree = GeneralUtility::makeInstance(TableConfigurationTree::class);
$tree->setDataProvider($treeDataProvider);
$tree->setNodeRenderer($treeRenderer);
// Merge tree nodes after calculated nodes from static items
$fieldConfig['config']['items'] = array_merge($finalItems, $tree->render());
}
$result['processedTca']['columns'][$fieldName] = $fieldConfig;
}
return $result;
}
/**
* Determines whether the current field is a valid target for this DataProvider
*
* @return bool
*/
protected function isTargetRenderType(array $fieldConfig)
{
return ($fieldConfig['config']['renderType'] ?? '') === 'selectTree';
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
/**
* Set the field shortcut to required if shortcut_mode is set to 0 (default)
*/
class TcaShortcut implements FormDataProviderInterface
{
public function addData(array $result): array
{
$tableName = $result['tableName'];
if ($tableName !== 'pages') {
return $result;
}
if (!in_array('shortcut', $result['columnsToProcess'])
|| !in_array('shortcut_mode', $result['columnsToProcess'])
) {
return $result;
}
$shortcutMode = $result['databaseRow']['shortcut_mode'] ?? null;
if ($shortcutMode !== null && (int)($shortcutMode[0] ?? $shortcutMode) === PageRepository::SHORTCUT_MODE_NONE) {
$result['processedTca']['columns']['shortcut']['config']['required'] = true;
}
return $result;
}
}
@@ -0,0 +1,350 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataCompiler;
use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Core\Configuration\Processor\Placeholder\EnvPlaceholderProcessor;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Schema\Field\FieldTypeInterface;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Resolve and prepare site language data
*
* @internal This FormDataProvider is only used in the site configuration module and is not public API
*/
class TcaSiteLanguage extends AbstractDatabaseRecordProvider implements FormDataProviderInterface
{
private const string FOREIGN_TABLE = 'site_language';
private const string FOREIGN_FIELD = 'languageId';
public function __construct(
private readonly SiteFinder $siteFinder,
private readonly InlineStackProcessor $inlineStackProcessor,
private readonly EnvPlaceholderProcessor $envPlaceholderProcessor,
) {}
public function addData(array $result): array
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (($fieldConfig['config']['type'] ?? '') !== 'siteLanguage') {
continue;
}
if (!$result['tcaSchemata']->has(self::FOREIGN_TABLE)) {
throw new \RuntimeException('Table ' . self::FOREIGN_TABLE . ' does not exists', 1624029932);
}
$foreignTableSchema = $result['tcaSchemata']->get(self::FOREIGN_TABLE);
/** @var FieldTypeInterface|null $foreignField */
$foreignField = $foreignTableSchema->hasField(self::FOREIGN_FIELD) ? $foreignTableSchema->getField(self::FOREIGN_FIELD) : null;
if ($foreignField?->getType() !== 'select') {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points to field '
. self::FOREIGN_FIELD . ' of table ' . self::FOREIGN_TABLE . ', but this field '
. 'is either not defined or is not of type select',
1624029933
);
}
if (!($foreignField->getConfiguration()['itemsProcFunc'] ?? false)) {
throw new \UnexpectedValueException(
'Table ' . $result['tableName'] . ' field ' . $fieldName . ' points to field '
. self::FOREIGN_FIELD . ' of table ' . self::FOREIGN_TABLE . '. This field must define '
. 'an \'itemsProcFunc\'.',
1624029934
);
}
$result = $this->addInlineRelatedConfig($result, $fieldName);
$result = $this->initializeMinMaxItems($result, $fieldName);
$result = $this->initializeAppearance($result, $fieldName);
$result = $this->addInlineFirstPid($result);
$result = $this->resolveSiteLanguageChildren($result, $fieldName);
$result = $this->addUniquePossibleRecords($result, $fieldName);
}
return $result;
}
protected function addInlineRelatedConfig(array $result, string $fieldName): array
{
$config = $result['processedTca']['columns'][$fieldName]['config'];
$config['foreign_table'] = self::FOREIGN_TABLE;
$config['foreign_selector'] = self::FOREIGN_FIELD;
$result['processedTca']['columns'][$fieldName]['config'] = $config;
return $result;
}
protected function initializeMinMaxItems(array $result, string $fieldName): array
{
$config = $result['processedTca']['columns'][$fieldName]['config'];
$config['minitems'] = isset($config['minitems']) ? MathUtility::forceIntegerInRange($config['minitems'], 1) : 1;
$config['maxitems'] = isset($config['maxitems']) ? MathUtility::forceIntegerInRange($config['maxitems'], 2) : 99999;
$result['processedTca']['columns'][$fieldName]['config'] = $config;
return $result;
}
protected function initializeAppearance(array $result, string $fieldName): array
{
$config = $result['processedTca']['columns'][$fieldName]['config'];
if (!is_array($config['appearance'] ?? false)) {
$config['appearance'] = [];
}
$config['appearance']['showPossibleLocalizationRecords'] = false;
$config['appearance']['collapseAll'] = true;
$config['appearance']['expandSingle'] = false;
$config['appearance']['enabledControls'] = [
'info' => false,
'new' => false,
'dragdrop' => false,
'sort' => false,
'hide' => false,
'delete' => true,
'localize' => false,
];
$config['size'] = (int)($config['size'] ?? 4);
$result['processedTca']['columns'][$fieldName]['config'] = $config;
return $result;
}
protected function addInlineFirstPid(array $result): array
{
if (($result['inlineFirstPid'] ?? null) !== null || ($result['tableName'] ?? '') !== self::FOREIGN_TABLE) {
return $result;
}
$pid = $result['databaseRow']['pid'] ?? 0;
if (!MathUtility::canBeInterpretedAsInteger($pid) || !str_starts_with($pid, 'NEW')) {
throw new \RuntimeException(
'inlineFirstPid should either be an integer or a "NEW..." string',
1624310264
);
}
$result['inlineFirstPid'] = $pid;
return $result;
}
protected function resolveSiteLanguageChildren(array $result, string $fieldName): array
{
$connectedUids = [];
$result['processedTca']['columns'][$fieldName]['children'] = [];
if ($result['command'] === 'edit') {
$unprocessedRootPageId = $result['databaseRow']['rootPageId'][0] ?? null;
$processedRootPageId = $this->envPlaceholderProcessor->canProcess($unprocessedRootPageId)
? (int)$this->envPlaceholderProcessor->process($unprocessedRootPageId)
: (int)$unprocessedRootPageId;
$siteConfiguration = [];
try {
$site = $this->siteFinder->getSiteByRootPageId($processedRootPageId);
$siteConfiguration = $site->getConfiguration();
} catch (SiteNotFoundException $e) {
}
if (is_array($siteConfiguration[$fieldName] ?? false)) {
// Add uids of existing site languages
$connectedUids = array_keys($siteConfiguration[$fieldName]);
}
} elseif ($result['command'] === 'new') {
// If new, *always* force a relation to the default language ("0")
$child = $this->compileDefaultSiteLanguageChild($result, $fieldName);
$connectedUids[] = $child['databaseRow']['uid'];
$result['processedTca']['columns'][$fieldName]['children'][] = $child;
}
// Add connected uids as csv field value
$result['databaseRow'][$fieldName] = implode(',', $connectedUids);
if ($result['inlineCompileExistingChildren']) {
foreach ($connectedUids as $uid) {
// Compile existing (persisted) site languages
if (!str_starts_with((string)$uid, 'NEW')) {
$compiledChild = $this->compileChild($result, $fieldName, $uid);
$result['processedTca']['columns'][$fieldName]['children'][] = $compiledChild;
}
}
}
if ($result['command'] === 'edit') {
// If edit, find out if a default language ("0") exists, else add it on top
$defaultSysSiteLanguageChildFound = false;
foreach ($result['processedTca']['columns'][$fieldName]['children'] as $child) {
if (isset($child['databaseRow']['languageId'][0]) && (int)$child['databaseRow']['languageId'][0] === 0) {
$defaultSysSiteLanguageChildFound = true;
}
}
if (!$defaultSysSiteLanguageChildFound) {
// Compile and add child as first child, since non exists yet
$child = $this->compileDefaultSiteLanguageChild($result, $fieldName);
$result['databaseRow'][$fieldName] = $child['databaseRow']['uid'] . ',' . $result['databaseRow'][$fieldName];
array_unshift($result['processedTca']['columns'][$fieldName]['children'], $child);
}
}
return $result;
}
protected function compileDefaultSiteLanguageChild(array $result, string $parentFieldName): array
{
$inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($result['inlineStructure'], 0);
return GeneralUtility::makeInstance(FormDataCompiler::class)
->compile(
[
'request' => $result['request'],
'command' => 'new',
'tableName' => self::FOREIGN_TABLE,
'vanillaUid' => $result['inlineFirstPid'],
'databaseRow' => $this->getDefaultDatabaseRow(),
'returnUrl' => $result['returnUrl'],
'isInlineChild' => true,
'inlineStructure' => [],
'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
'inlineFirstPid' => $result['inlineFirstPid'],
'inlineParentConfig' => $result['processedTca']['columns'][$parentFieldName]['config'],
'inlineParentUid' => $result['databaseRow']['uid'],
'inlineParentTableName' => $result['tableName'],
'inlineParentFieldName' => $parentFieldName,
'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: ($inlineTopMostParent['uid'] ?? null),
'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: ($inlineTopMostParent['table'] ?? ''),
'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: ($inlineTopMostParent['field'] ?? ''),
'inlineChildChildUid' => 0,
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
],
GeneralUtility::makeInstance(SiteConfigurationDataGroup::class)
);
}
protected function compileChild(array $result, string $parentFieldName, int $childUid): array
{
$inlineTopMostParent = $this->inlineStackProcessor->getStructureLevelFromStructure($result['inlineStructure'], 0);
return GeneralUtility::makeInstance(FormDataCompiler::class)
->compile(
[
'request' => $result['request'],
'command' => 'edit',
'tableName' => self::FOREIGN_TABLE,
'vanillaUid' => $childUid,
'returnUrl' => $result['returnUrl'],
'isInlineChild' => true,
'inlineStructure' => $result['inlineStructure'],
'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
'inlineFirstPid' => $result['inlineFirstPid'],
'inlineParentConfig' => $result['processedTca']['columns'][$parentFieldName]['config'],
'inlineParentUid' => $result['databaseRow']['uid'],
'inlineParentTableName' => $result['tableName'],
'inlineParentFieldName' => $parentFieldName,
'inlineTopMostParentUid' => $result['inlineTopMostParentUid'] ?: ($inlineTopMostParent['uid'] ?? null),
'inlineTopMostParentTableName' => $result['inlineTopMostParentTableName'] ?: ($inlineTopMostParent['table'] ?? ''),
'inlineTopMostParentFieldName' => $result['inlineTopMostParentFieldName'] ?: ($inlineTopMostParent['field'] ?? ''),
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
],
GeneralUtility::makeInstance(SiteConfigurationDataGroup::class)
);
}
protected function addUniquePossibleRecords(array $result, string $fieldName): array
{
$formDataGroup = GeneralUtility::makeInstance(OnTheFly::class);
$formDataGroup->setProviderList([TcaSelectItems::class]);
$foreignTableSchema = $result['tcaSchemata']->get(self::FOREIGN_TABLE);
// Add unique possible records, so they can be used in the selector field
$result['processedTca']['columns'][$fieldName]['config']['uniquePossibleRecords'] = GeneralUtility::makeInstance(FormDataCompiler::class)
->compile(
[
'request' => $result['request'],
'command' => 'new',
'tableName' => self::FOREIGN_TABLE,
'pageTsConfig' => $result['pageTsConfig'],
'userTsConfig' => $result['userTsConfig'],
'databaseRow' => $result['databaseRow'],
'processedTca' => [
'ctrl' => [],
'columns' => [
self::FOREIGN_FIELD => [
'config' => $foreignTableSchema->getField(self::FOREIGN_FIELD)->getConfiguration(),
],
],
],
'inlineExpandCollapseStateArray' => $result['inlineExpandCollapseStateArray'],
// pass through schemata as they are immutable once they are set
'tcaSchemata' => $result['tcaSchemata'],
// pass through fullTca as it is immutable once set
'fullTca' => $result['fullTca'],
],
$formDataGroup
)['processedTca']['columns'][self::FOREIGN_FIELD]['config']['items'] ?? [];
return $result;
}
/**
* Create the database row for the default site language based
* on an already existing default language from another site.
*/
protected function getDefaultDatabaseRow(): array
{
$defaultDatabaseRow = [];
foreach ($this->siteFinder->getAllSites() as $site) {
foreach ($site->getAllLanguages() as $language) {
if ($language->getLanguageId() === 0) {
$defaultDatabaseRow['locale'] = $language->getLocale()->posixFormatted();
if ($language->getTitle() !== '') {
$defaultDatabaseRow['title'] = $language->getTitle();
}
if ($language->getNavigationTitle() !== '') {
$defaultDatabaseRow['navigationTitle'] = $language->getNavigationTitle();
}
if ($language->getHreflang(true) !== '') {
$defaultDatabaseRow['hreflang'] = $language->getHreflang();
}
if (str_starts_with($language->getFlagIdentifier(), 'flags-')) {
$flagIdentifier = str_replace('flags-', '', $language->getFlagIdentifier());
$defaultDatabaseRow['flag'] = ($flagIdentifier === 'multiple') ? 'global' : $flagIdentifier;
}
break 2;
}
}
}
return $defaultDatabaseRow;
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Handles custom data for TCA Type=Slug
*/
class TcaSlug implements FormDataProviderInterface
{
/**
* Resolve slug prefix items
*
*
* @throws \UnexpectedValueException
*/
public function addData(array $result): array
{
$table = $result['tableName'];
$site = $result['site'];
$row = $result['databaseRow'];
$languageId = 0;
if (($languageField = $result['processedTca']['ctrl']['languageField'] ?? '') !== '' && isset($row[$languageField])) {
$languageId = (int)(is_array($row[$languageField]) ? ($row[$languageField][0] ?? 0) : $row[$languageField]);
}
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (($fieldConfig['config']['type'] ?? '') !== 'slug') {
continue;
}
$prefixUserFunc = $fieldConfig['config']['appearance']['prefix'] ?? '';
if ($prefixUserFunc !== '') {
$parameters = [
'site' => $site,
'languageId' => $languageId,
'table' => $table,
'row' => $row,
'fieldName' => $fieldName,
'config' => $fieldConfig['config'],
];
$prefix = GeneralUtility::callUserFunction($prefixUserFunc, $parameters, $this);
} elseif ($site instanceof SiteInterface) {
// default behaviour used for pages
$prefix = $this->getPrefixForSite($site, $languageId);
} else {
// no site found, so we cannot determine a prefix
$prefix = '';
}
$result['customData'][$fieldName]['slugPrefix'] = $prefix;
$result['processedTca']['columns'][$fieldName]['config']['appearance']['prefix'] = $prefix;
}
return $result;
}
/**
* Render the prefix for the input field.
*/
protected function getPrefixForSite(SiteInterface $site, int $languageId): string
{
try {
$language = ($languageId < 0) ? $site->getDefaultLanguage() : $site->getLanguageById($languageId);
$base = $language->getBase();
$prefix = rtrim((string)$base, '/');
if ($prefix !== '' && empty($base->getScheme()) && $base->getHost() !== '') {
$prefix = 'http:' . $prefix;
}
} catch (\InvalidArgumentException $e) {
// No site found
$prefix = '';
}
return $prefix;
}
}
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Data provider for type=select + renderType=tablePermission fields.
*
* @internal Only used for be_groups "tablePermission" renderType.
*/
final class TcaTablePermission extends AbstractItemProvider implements FormDataProviderInterface
{
public function addData(array $result): array
{
$table = $result['tableName'];
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (($fieldConfig['config']['type'] ?? '') !== 'select'
|| ($fieldConfig['config']['renderType'] ?? '') !== 'tablePermission'
) {
continue;
}
if (!$result['tcaSchemata']->has($table) || !$result['tcaSchemata']->get($table)->hasField($fieldConfig['config']['selectFieldName'] ?? null)) {
throw new \InvalidArgumentException(
'renderType="tablePermission" requires option "selectFieldName" to be set to an existing column of table ' . $table,
1720028589
);
}
$result['databaseRow'][$fieldName] = [
'modify' => array_values(array_unique($this->processDatabaseFieldValue($result['databaseRow'], $fieldName))),
'select' => array_values(array_unique($this->processDatabaseFieldValue($result['databaseRow'], $fieldConfig['config']['selectFieldName']))),
];
}
return $result;
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Configuration\Richtext;
use TYPO3\CMS\Core\Html\RteHtmlParser;
/**
* Resolve databaseRow field content for type=text, especially handle
* richtext transformations "from db to rte"
*/
readonly class TcaText implements FormDataProviderInterface
{
public function __construct(
private Richtext $richtext,
private RteHtmlParser $rteHtmlParser,
) {}
/**
* Handle text field content, especially richtext transformation
*
* @param array $result Given result array
* @return array Modified result array
*/
public function addData(array $result)
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (empty($fieldConfig['config']['type']) || $fieldConfig['config']['type'] !== 'text') {
continue;
}
// Check if richtext is enabled for the field
if ($fieldConfig['config']['enableRichtext'] ?? false) {
$richtextConfiguration = $this->richtext->getConfiguration(
$result['tableName'],
$fieldName,
$result['effectivePid'],
(string)$result['recordTypeValue'],
$fieldConfig['config']
);
// Transform if richtext is not disabled in configuration
if (!($richtextConfiguration['disabled'] ?? false)) {
// remember RTE preset name
$result['processedTca']['columns'][$fieldName]['config']['richtextConfigurationName'] = $fieldConfig['config']['richtextConfiguration'] ?? '';
// Add final resolved configuration to TCA array
$result['processedTca']['columns'][$fieldName]['config']['richtextConfiguration'] = $richtextConfiguration;
// If eval=null is set for field, value might be null ... don't transform anything in this case.
if ($result['databaseRow'][$fieldName] !== null) {
// Process "from-db-to-rte" on current value
$result['databaseRow'][$fieldName] = $this->rteHtmlParser->transformTextForRichTextEditor($result['databaseRow'][$fieldName], $richtextConfiguration['proc.'] ?? []);
}
}
}
}
return $result;
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Reduce tt_content colPos items if needed: When the pages backend layout does not allow
* the current content element type (CType value) in a colPos (via "allowedContentTypes" and
* "disallowedContentTypes" backend layout column configuration), then the content element
* can not be switched to this colPos. The implementation reduces colPos items accordingly.
*
* @internal
*/
final readonly class TcaTtContentColPosItemsRestrictionByBackendLayout implements FormDataProviderInterface
{
public function __construct(
private BackendLayoutView $backendLayoutView,
) {}
public function addData(array $result): array
{
if ($result['tableName'] !== 'tt_content'
|| empty($result['databaseRow']['CType'])
|| !empty($result['isInlineChild'])
|| empty($result['processedTca']['columns']['colPos']['config']['type'])
|| $result['processedTca']['columns']['colPos']['config']['type'] !== 'select'
|| empty($result['processedTca']['columns']['colPos']['config']['items'])
|| !is_array($result['processedTca']['columns']['colPos']['config']['items'])
) {
// tt_content colPos should be select. Return early if it isn't for some reason, or if tt_content is an inline child
return $result;
}
$languageService = $this->getLanguageService();
$pageId = !empty($result['effectivePid']) ? (int)$result['effectivePid'] : (int)$result['databaseRow']['pid'];
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageId);
$recordType = $result['databaseRow']['CType'][0] ?? '';
$currentColPos = (int)($result['databaseRow']['colPos'][0] ?? 0);
foreach ($result['processedTca']['columns']['colPos']['config']['items'] as $key => $item) {
$itemColPosValue = (int)($item['value']);
$columnConfiguration = $this->backendLayoutView->getColPosConfigurationForPage($backendLayout, $itemColPosValue, $pageId, $result['request']);
if (empty($columnConfiguration)) {
continue;
}
if (!empty($columnConfiguration['allowedContentTypes'])) {
$allowedContentTypes = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'], true);
if (!in_array($recordType, $allowedContentTypes, true)
&& $currentColPos !== $itemColPosValue
) {
unset($result['processedTca']['columns']['colPos']['config']['items'][$key]);
}
if (!in_array($recordType, $allowedContentTypes, true)
&& $currentColPos === $itemColPosValue
) {
$newLabel = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $currentColPos);
$result['processedTca']['columns']['colPos']['config']['items'][$key]['label'] = $newLabel;
}
}
if (!empty($columnConfiguration['disallowedContentTypes'])) {
$disallowedContentTypes = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'], true);
if (in_array($recordType, $disallowedContentTypes, true)
&& $currentColPos !== $itemColPosValue
) {
unset($result['processedTca']['columns']['colPos']['config']['items'][$key]);
}
if (in_array($recordType, $disallowedContentTypes, true)
&& $currentColPos === $itemColPosValue
) {
$newLabel = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $currentColPos);
$result['processedTca']['columns']['colPos']['config']['items'][$key]['label'] = $newLabel;
}
}
}
return $result;
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Backend\View\BackendLayoutView;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Reduce tt_content CType items if needed: When the pages backend layout does not allow
* content element types in current record colPos (via "allowedContentTypes" and
* "disallowedContentTypes" backend layout column configuration), then the content element
* can not be switched into those types. The implementation reduces CType items accordingly.
*
* @internal
*/
final readonly class TcaTtContentCtypeItemsRestrictionByBackendLayout implements FormDataProviderInterface
{
public function __construct(
private BackendLayoutView $backendLayoutView,
) {}
public function addData(array $result): array
{
if ($result['tableName'] !== 'tt_content'
|| empty($result['databaseRow']['CType'])
|| !empty($result['isInlineChild'])
|| empty($result['processedTca']['columns']['CType']['config']['items'])
) {
return $result;
}
$languageService = $this->getLanguageService();
$pageId = !empty($result['effectivePid']) ? (int)$result['effectivePid'] : (int)$result['databaseRow']['pid'];
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageId);
if (is_array($result['databaseRow']['colPos'] ?? [])) {
$currentColPosValue = (int)($result['databaseRow']['colPos'][0] ?? $result['processedTca']['columns']['colPos']['config']['default'] ?? 0);
} else {
$currentColPosValue = (int)($result['databaseRow']['colPos'] ?? $result['processedTca']['columns']['colPos']['config']['default'] ?? 0);
}
$columnConfiguration = $this->backendLayoutView->getColPosConfigurationForPage($backendLayout, $currentColPosValue, $pageId, $result['request']);
$currentRecordType = $result['databaseRow']['CType'][0] ?? '';
if (!empty($columnConfiguration['allowedContentTypes'])) {
$allowedContentTypes = GeneralUtility::trimExplode(',', $columnConfiguration['allowedContentTypes'], true);
foreach ($result['processedTca']['columns']['CType']['config']['items'] as $itemKey => $item) {
if (!in_array($item['value'], $allowedContentTypes, true)
&& $item['value'] !== '--div--'
&& $item['value'] !== $currentRecordType
) {
unset($result['processedTca']['columns']['CType']['config']['items'][$itemKey]);
}
if (!in_array($item['value'], $allowedContentTypes, true)
&& $item['value'] !== '--div--'
&& $item['value'] === $currentRecordType
) {
$newLabel = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $currentRecordType);
$result['processedTca']['columns']['CType']['config']['items'][$itemKey]['label'] = $newLabel;
}
}
}
if (!empty($columnConfiguration['disallowedContentTypes'])) {
$disallowedContentTypes = GeneralUtility::trimExplode(',', $columnConfiguration['disallowedContentTypes'], true);
foreach ($result['processedTca']['columns']['CType']['config']['items'] as $itemKey => $item) {
if (in_array($item['value'], $disallowedContentTypes, true)
&& $item['value'] !== $currentRecordType
) {
unset($result['processedTca']['columns']['CType']['config']['items'][$itemKey]);
}
if (in_array($item['value'], $disallowedContentTypes, true)
&& $item['value'] === $currentRecordType
) {
$newLabel = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'), $currentRecordType);
$result['processedTca']['columns']['CType']['config']['items'][$itemKey]['label'] = $newLabel;
}
}
}
return $result;
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Merge type-specific ctrl configuration from the types section into processedTca ctrl section.
*
* This allows tables to define type-specific ctrl properties in the types section
* that override the global ctrl values for that specific record type.
*
* @todo This data provider is just an intermediate solution until FormEngine is using TCA Schema
*/
class TcaTypesCtrlOverrides implements FormDataProviderInterface
{
/**
* List of ctrl properties that can be overridden per type.
* This prevents arbitrary ctrl properties from being overridden which might
* cause issues if they affect the structure or behavior of TCA processing.
*
* Currently supported: 'title' and 'previewRenderer'.
* Additional properties may be added in the future.
*/
protected array $allowedCtrlOverrides = [
'title',
'previewRenderer',
];
public function addData(array $result): array
{
$type = $result['recordTypeValue'];
if (isset($result['processedTca']['types'][$type]) && is_array($result['processedTca']['types'][$type])) {
$typeConfiguration = $result['processedTca']['types'][$type];
// Merge allowed ctrl properties from type configuration into ctrl section
foreach ($this->allowedCtrlOverrides as $property) {
if (array_key_exists($property, $typeConfiguration)) {
$result['processedTca']['ctrl'][$property] = $typeConfiguration[$property];
}
}
}
return $result;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* 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 Symfony\Component\Uid\Uuid;
use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
/**
* Generates and sets field value for type=uuid
*/
class TcaUuid implements FormDataProviderInterface
{
public function addData(array $result): array
{
foreach ($result['processedTca']['columns'] as $fieldName => $fieldConfig) {
if (($fieldConfig['config']['type'] ?? '') !== 'uuid') {
continue;
}
// Skip if field is already filled with a valid uuid
if (Uuid::isValid((string)($result['databaseRow'][$fieldName] ?? ''))) {
continue;
}
if ($fieldConfig['config']['required'] ?? true) {
$result['databaseRow'][$fieldName] = (string)match ((int)($fieldConfig['config']['version'] ?? 0)) {
6 => Uuid::v6(),
7 => Uuid::v7(),
default => Uuid::v4()
};
}
}
return $result;
}
}
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/*
* 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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\UserSettingsSchema;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
/**
* FormDataProvider for backend user settings.
*
* Loads user data from BE_USER->user and BE_USER->uc into databaseRow
* for the user settings form.
*
* @internal
*/
readonly class UserSettingsDatabaseEditRow implements FormDataProviderInterface
{
public function __construct(
private UserSettingsSchema $userSettingsSchema,
private ConnectionPool $connectionPool,
) {}
public function addData(array $result): array
{
if ($result['command'] !== 'edit' || $result['tableName'] !== 'be_users_settings') {
return $result;
}
$backendUser = $this->getBackendUser();
$userSettings = $backendUser->getUserSettings()->toArray();
$userSettingsColumns = $this->userSettingsSchema->getColumns();
$jsonFieldSettingKeys = $this->userSettingsSchema->getJsonFieldSettingKeys();
// Also provide direct access to be_users fields that are shown in the form
// These are needed for fields with inheritFromParent=true
foreach ($userSettingsColumns as $column => $config) {
$partitionedColumnName = $this->userSettingsSchema->getTcaFieldName($column);
if (isset($backendUser->user[$column])) {
$result['databaseRow'][$partitionedColumnName] = $backendUser->user[$column];
} elseif (isset($userSettings[$column])) {
$result['databaseRow'][$partitionedColumnName] = $userSettings[$column];
}
}
// Set the uid from the current user
$result['databaseRow']['uid'] = (int)$backendUser->user['uid'];
$result['databaseRow']['pid'] = 0;
// Fill in random to passwords to avoid FormEngine issuing the required field error
$randomPassword = bin2hex(random_bytes(20));
$passwordFieldName = $this->userSettingsSchema->getTcaFieldName('password');
$result['databaseRow'][$passwordFieldName] = $randomPassword;
$passwordConfirmationFieldName = $this->userSettingsSchema->getTcaFieldName('password2');
$result['databaseRow'][$passwordConfirmationFieldName] = $randomPassword;
// Forward the avatar FAL id
$avatarFieldName = $this->userSettingsSchema->getTcaFieldName('avatar');
$result['databaseRow'][$avatarFieldName] = $this->getAvatarFileUid((int)$backendUser->user['uid']);
return $result;
}
protected function getAvatarFileUid(int $beUserId): int
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_file_reference');
$file = $queryBuilder->select('uid_local')
->from('sys_file_reference')
->where(
$queryBuilder->expr()->eq(
'tablenames',
$queryBuilder->createNamedParameter('be_users')
),
$queryBuilder->expr()->eq(
'fieldname',
$queryBuilder->createNamedParameter('avatar')
),
$queryBuilder->expr()->eq(
'uid_foreign',
$queryBuilder->createNamedParameter($beUserId, Connection::PARAM_INT)
)
)
->executeQuery()
->fetchOne();
return (int)$file;
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,41 @@
<?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 TYPO3\CMS\Backend\Form\FormDataProviderInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* Add user TSconfig to result
*/
class UserTsConfig implements FormDataProviderInterface
{
/**
* Add user typo script config
*
* @return array
*/
public function addData(array $result)
{
$result['userTsConfig'] = $this->getBackendUser()->getTSConfig();
return $result;
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}