328 lines
14 KiB
PHP
328 lines
14 KiB
PHP
<?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\Controller\Wizard;
|
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use TYPO3\CMS\Backend\Attribute\AsController;
|
|
use TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver;
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
|
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
|
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
|
use TYPO3\CMS\Core\Schema\Capability\RootLevelCapability;
|
|
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
|
use TYPO3\CMS\Core\Schema\TcaSchema;
|
|
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
|
use TYPO3\CMS\Core\Security\RawValue;
|
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
|
|
/**
|
|
* Receives ajax request from FormEngine suggest wizard and creates suggest answer as json result
|
|
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
|
*/
|
|
#[AsController]
|
|
readonly class SuggestWizardController
|
|
{
|
|
public function __construct(
|
|
private FlexFormTools $flexFormTools,
|
|
private TcaSchemaFactory $tcaSchemaFactory,
|
|
private ConnectionPool $connectionPool,
|
|
) {}
|
|
|
|
/**
|
|
* Ajax handler for the "suggest" feature in FormEngine.
|
|
*
|
|
* @throws \RuntimeException for incomplete or invalid arguments
|
|
*/
|
|
public function searchAction(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$parsedBody = $request->getParsedBody();
|
|
|
|
$search = $parsedBody['value'] ?? null;
|
|
$tableName = $parsedBody['tableName'] ?? null;
|
|
$fieldName = $parsedBody['fieldName'] ?? null;
|
|
$uid = $parsedBody['uid'] ?? null;
|
|
$pid = isset($parsedBody['pid']) ? (int)$parsedBody['pid'] : 0;
|
|
$dataStructureIdentifier = $parsedBody['dataStructureIdentifier'] ?? '';
|
|
$flexFormSheetName = $parsedBody['flexFormSheetName'] ?? null;
|
|
$flexFormFieldName = $parsedBody['flexFormFieldName'] ?? null;
|
|
$flexFormContainerName = $parsedBody['flexFormContainerName'] ?? null;
|
|
$flexFormContainerFieldName = $parsedBody['flexFormContainerFieldName'] ?? null;
|
|
$recordType = (string)($parsedBody['recordTypeValue'] ?? '') ?: null;
|
|
$schema = $this->tcaSchemaFactory->get($tableName);
|
|
|
|
// Determine TCA config of field
|
|
if (empty($dataStructureIdentifier)) {
|
|
// Normal columns field
|
|
$fieldInformation = $schema->getField($fieldName);
|
|
$fieldConfig = $fieldInformation->getConfiguration();
|
|
$fieldNameInPageTsConfig = $fieldName;
|
|
|
|
// With possible columnsOverrides
|
|
// @todo Validate if we can move this fallback recordType determination, should be do-able in v13?!
|
|
if ($recordType === null) {
|
|
$recordType = BackendUtility::getTCAtypeValue(
|
|
$tableName,
|
|
BackendUtility::getRecord($tableName, $uid) ?? [],
|
|
true
|
|
);
|
|
}
|
|
if ($recordType !== null && $schema->hasSubSchema($recordType)) {
|
|
$fieldConfig = $schema->getSubSchema($recordType)->getField($fieldName)->getConfiguration();
|
|
}
|
|
} else {
|
|
// A flex-form field
|
|
$dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier, $schema);
|
|
if (empty($flexFormContainerFieldName)) {
|
|
// @todo: See if a path in pageTsConfig like "TCEForm.tableName.theContainerFieldName =" is useful and works with other pageTs, too.
|
|
$fieldNameInPageTsConfig = $flexFormFieldName;
|
|
if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
|
['el'][$flexFormFieldName]['config'])
|
|
) {
|
|
throw new \RuntimeException(
|
|
'Specified path ' . $flexFormFieldName . ' not found in flex form data structure',
|
|
1480609491
|
|
);
|
|
}
|
|
$fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
|
['el'][$flexFormFieldName]['config'];
|
|
} else {
|
|
$fieldNameInPageTsConfig = $flexFormContainerFieldName;
|
|
if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
|
['el'][$flexFormFieldName]
|
|
['el'][$flexFormContainerName]
|
|
['el'][$flexFormContainerFieldName]['config'])
|
|
) {
|
|
throw new \RuntimeException(
|
|
'Specified path ' . $flexFormContainerName . ' not found in flex form section container data structure',
|
|
1480611208
|
|
);
|
|
}
|
|
$fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
|
['el'][$flexFormFieldName]
|
|
['el'][$flexFormContainerName]
|
|
['el'][$flexFormContainerFieldName]['config'];
|
|
}
|
|
}
|
|
|
|
$pageTsConfig = BackendUtility::getPagesTSconfig($pid);
|
|
|
|
$wizardConfig = $fieldConfig['suggestOptions'] ?? [];
|
|
|
|
$queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
|
|
$whereClause = $this->getWhereClause($fieldConfig);
|
|
|
|
$resultRows = [];
|
|
|
|
// fetch the records for each query table. A query table is a table from which records are allowed to
|
|
// be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
|
|
foreach ($queryTables as $queryTable) {
|
|
// if the table does not exist, skip it
|
|
if (!$this->tcaSchemaFactory->has($queryTable)) {
|
|
continue;
|
|
}
|
|
|
|
$config = $this->getConfigurationForTable($queryTable, $wizardConfig, $pageTsConfig, $tableName, $fieldNameInPageTsConfig);
|
|
|
|
// process addWhere
|
|
if (!isset($config['addWhere']) && $whereClause) {
|
|
$config['addWhere'] = $whereClause;
|
|
}
|
|
if (isset($config['addWhere'])) {
|
|
$replacement = [
|
|
'###THIS_UID###' => (int)$uid,
|
|
'###CURRENT_PID###' => (int)$pid,
|
|
];
|
|
if (isset($pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'])) {
|
|
$fieldTSconfig = $pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'];
|
|
if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
|
|
$replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
|
|
}
|
|
if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
|
|
$replacement['###PAGE_TSCONFIG_IDLIST###'] = implode(',', GeneralUtility::intExplode(',', (string)$fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
|
|
}
|
|
if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
|
|
$connection = $this->connectionPool->getConnectionForTable($fieldConfig['foreign_table']);
|
|
// nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping
|
|
// the whole field as it is not known where it is used in the WHERE clause
|
|
$replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\'');
|
|
}
|
|
}
|
|
$config['addWhere'] = QueryHelper::quoteDatabaseIdentifiers($this->connectionPool->getConnectionForTable($queryTable), strtr(' ' . $config['addWhere'], $replacement));
|
|
}
|
|
|
|
// instantiate the class that should fetch the records for this $queryTable
|
|
$receiverClassName = $config['receiverClass'] ?? '';
|
|
if (!class_exists($receiverClassName)) {
|
|
$receiverClassName = SuggestWizardDefaultReceiver::class;
|
|
}
|
|
$receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
|
|
$params = [
|
|
'value' => $search,
|
|
'uid' => $uid,
|
|
];
|
|
$rows = $receiverObj->queryTable($params);
|
|
if (empty($rows)) {
|
|
continue;
|
|
}
|
|
$resultRows = $rows + $resultRows;
|
|
unset($rows);
|
|
}
|
|
|
|
// Limit the number of items in the result list
|
|
$maxItems = (int)($config['maxItemsInResultList'] ?? 10);
|
|
$maxItems = min(count($resultRows), $maxItems);
|
|
|
|
array_splice($resultRows, $maxItems);
|
|
return new JsonResponse(array_values($resultRows));
|
|
}
|
|
|
|
/**
|
|
* Checks if the current backend user is allowed to access the given table, based on the schema capabilities.
|
|
*/
|
|
protected function currentBackendUserMayAccessTable(TcaSchema $schema): bool
|
|
{
|
|
if ($this->getBackendUser()->isAdmin()) {
|
|
return true;
|
|
}
|
|
|
|
// If the user is no admin, they may not access admin-only tables
|
|
if ($schema->hasCapability(TcaSchemaCapability::AccessAdminOnly)) {
|
|
return false;
|
|
}
|
|
|
|
/** @var RootLevelCapability $rootLevelCapability */
|
|
$rootLevelCapability = $schema->getCapability(TcaSchemaCapability::RestrictionRootLevel);
|
|
|
|
// allow access to root level pages if security restrictions should be bypassed
|
|
return $rootLevelCapability->canAccessRecordsOnRootLevel();
|
|
}
|
|
|
|
/**
|
|
* Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the
|
|
* TSconfig.
|
|
*
|
|
* @param string $queryTable The table to query
|
|
* @param array $wizardConfig The configuration for the wizard as configured in the data structure
|
|
* @param array $TSconfig The TSconfig array of the current page
|
|
* @param string $table The table where the wizard is used
|
|
* @param string $field The field where the wizard is used
|
|
*/
|
|
protected function getConfigurationForTable(string $queryTable, array $wizardConfig, array $TSconfig, string $table, string $field): array
|
|
{
|
|
$config = (array)($wizardConfig['default'] ?? []);
|
|
|
|
if (is_array($wizardConfig[$queryTable] ?? null)) {
|
|
ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
|
|
}
|
|
|
|
$globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'] ?? [];
|
|
$currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'] ?? [];
|
|
|
|
// merge the configurations of different "levels" to get the working configuration for this table and
|
|
// field (i.e., go from the most general to the most special configuration)
|
|
if (is_array($globalSuggestTsConfig['default.'] ?? null)) {
|
|
ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($globalSuggestTsConfig['default.']));
|
|
}
|
|
|
|
if (is_array($globalSuggestTsConfig[$queryTable . '.'] ?? null)) {
|
|
ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($globalSuggestTsConfig[$queryTable . '.']));
|
|
}
|
|
|
|
// use $table instead of $queryTable here because we overlay a config
|
|
// for the input-field here, not for the queried table
|
|
if (is_array($currentFieldSuggestTsConfig['default.'] ?? null)) {
|
|
ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($currentFieldSuggestTsConfig['default.']));
|
|
}
|
|
|
|
if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'] ?? null)) {
|
|
ArrayUtility::mergeRecursiveWithOverrule($config, $this->substituteRawValues($currentFieldSuggestTsConfig[$queryTable . '.']));
|
|
}
|
|
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* Checks the given field configuration for the tables that should be used for querying and returns them as an
|
|
* array.
|
|
*/
|
|
protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig): array
|
|
{
|
|
$queryTables = [];
|
|
|
|
if (isset($fieldConfig['allowed'])) {
|
|
if ($fieldConfig['allowed'] !== '*') {
|
|
// list of allowed tables
|
|
$queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
|
|
} else {
|
|
// all tables are allowed, if the user can access them
|
|
/** @var TcaSchema $schema */
|
|
foreach ($this->tcaSchemaFactory->all() as $tableName => $schema) {
|
|
if ($schema->hasCapability(TcaSchemaCapability::HideInUi)) {
|
|
continue;
|
|
}
|
|
if ($this->currentBackendUserMayAccessTable($schema)) {
|
|
$queryTables[] = $tableName;
|
|
}
|
|
}
|
|
}
|
|
} elseif (isset($fieldConfig['foreign_table'])) {
|
|
// use the foreign table
|
|
$queryTables = [$fieldConfig['foreign_table']];
|
|
}
|
|
|
|
return $queryTables;
|
|
}
|
|
|
|
/**
|
|
* Wraps user functions in the configuration array as a `RawValue` object,
|
|
* to be asserted later when actually calling `GeneralUtility::callUserFunction`.
|
|
*/
|
|
protected function substituteRawValues(array $config): array
|
|
{
|
|
if (!empty($config['renderFunc'])) {
|
|
$config['renderFunc'] = new RawValue($config['renderFunc']);
|
|
}
|
|
return $config;
|
|
}
|
|
|
|
/**
|
|
* Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table
|
|
* is configured and should be used; it could e.g. be used to limit to a certain subset of records from the
|
|
* foreign table
|
|
*/
|
|
protected function getWhereClause(array $fieldConfig): string
|
|
{
|
|
if (!isset($fieldConfig['foreign_table'], $fieldConfig['foreign_table_where'])) {
|
|
return '';
|
|
}
|
|
|
|
// strip ORDER BY clause
|
|
return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
|
|
}
|
|
|
|
protected function getBackendUser(): BackendUserAuthentication
|
|
{
|
|
return $GLOBALS['BE_USER'];
|
|
}
|
|
}
|