TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user