‪TYPO3CMS  ‪main
SuggestWizardController.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ResponseInterface;
21 use Psr\Http\Message\ServerRequestInterface;
24 use TYPO3\CMS\Backend\Utility\BackendUtility;
32 
37 #[AsController]
39 {
40  public function ‪__construct(
41  private readonly ‪FlexFormTools $flexFormTools,
42  ) {}
43 
49  public function ‪searchAction(ServerRequestInterface $request): ResponseInterface
50  {
51  $parsedBody = $request->getParsedBody();
52 
53  $search = $parsedBody['value'] ?? null;
54  $tableName = $parsedBody['tableName'] ?? null;
55  $fieldName = $parsedBody['fieldName'] ?? null;
56  ‪$uid = $parsedBody['uid'] ?? null;
57  $pid = isset($parsedBody['pid']) ? (int)$parsedBody['pid'] : 0;
58  $dataStructureIdentifier = $parsedBody['dataStructureIdentifier'] ?? '';
59  $flexFormSheetName = $parsedBody['flexFormSheetName'] ?? null;
60  $flexFormFieldName = $parsedBody['flexFormFieldName'] ?? null;
61  $flexFormContainerName = $parsedBody['flexFormContainerName'] ?? null;
62  $flexFormContainerFieldName = $parsedBody['flexFormContainerFieldName'] ?? null;
63  $recordType = (string)($parsedBody['recordTypeValue'] ?? '');
64 
65  // Determine TCA config of field
66  if (empty($dataStructureIdentifier)) {
67  // Normal columns field
68  $fieldConfig = ‪$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
69  $fieldNameInPageTsConfig = $fieldName;
70 
71  // With possible columnsOverrides
72  // @todo Validate if we can move this fallback recordType determination, should be do-able in v13?!
73  if ($recordType === '') {
74  $recordType = BackendUtility::getTCAtypeValue(
75  $tableName,
76  BackendUtility::getRecord($tableName, ‪$uid) ?? []
77  );
78  }
79  $columnsOverridesConfigOfField = ‪$GLOBALS['TCA'][$tableName]['types'][$recordType]['columnsOverrides'][$fieldName]['config'] ?? null;
80  if ($columnsOverridesConfigOfField) {
81  ArrayUtility::mergeRecursiveWithOverrule($fieldConfig, $columnsOverridesConfigOfField);
82  }
83  } else {
84  // A flex flex form field
85  $dataStructure = $this->flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
86  if (empty($flexFormContainerFieldName)) {
87  // @todo: See if a path in pageTsConfig like "TCEForm.tableName.theContainerFieldName =" is useful and works with other pageTs, too.
88  $fieldNameInPageTsConfig = $flexFormFieldName;
89  if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
90  ['el'][$flexFormFieldName]['config'])
91  ) {
92  throw new \RuntimeException(
93  'Specified path ' . $flexFormFieldName . ' not found in flex form data structure',
94  1480609491
95  );
96  }
97  $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
98  ['el'][$flexFormFieldName]['config'];
99  } else {
100  $fieldNameInPageTsConfig = $flexFormContainerFieldName;
101  if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
102  ['el'][$flexFormFieldName]
103  ['el'][$flexFormContainerName]
104  ['el'][$flexFormContainerFieldName]['config'])
105  ) {
106  throw new \RuntimeException(
107  'Specified path ' . $flexFormContainerName . ' not found in flex form section container data structure',
108  1480611208
109  );
110  }
111  $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
112  ['el'][$flexFormFieldName]
113  ['el'][$flexFormContainerName]
114  ['el'][$flexFormContainerFieldName]['config'];
115  }
116  }
117 
118  $pageTsConfig = BackendUtility::getPagesTSconfig($pid);
119 
120  $wizardConfig = $fieldConfig['suggestOptions'] ?? [];
121 
122  $queryTables = $this->‪getTablesToQueryFromFieldConfiguration($fieldConfig);
123  $whereClause = $this->‪getWhereClause($fieldConfig);
124 
125  $resultRows = [];
126 
127  // fetch the records for each query table. A query table is a table from which records are allowed to
128  // be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
129  foreach ($queryTables as $queryTable) {
130  // if the table does not exist, skip it
131  if (!is_array(‪$GLOBALS['TCA'][$queryTable]) || empty(‪$GLOBALS['TCA'][$queryTable])) {
132  continue;
133  }
134 
135  $config = $this->‪getConfigurationForTable($queryTable, $wizardConfig, $pageTsConfig, $tableName, $fieldNameInPageTsConfig);
136 
137  // process addWhere
138  if (!isset($config['addWhere']) && $whereClause) {
139  $config['addWhere'] = $whereClause;
140  }
141  if (isset($config['addWhere'])) {
142  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
143  $replacement = [
144  '###THIS_UID###' => (int)‪$uid,
145  '###CURRENT_PID###' => (int)$pid,
146  ];
147  if (isset($pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'])) {
148  $fieldTSconfig = $pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'];
149  if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
150  $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
151  }
152  if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
153  $replacement['###PAGE_TSCONFIG_IDLIST###'] = implode(',', ‪GeneralUtility::intExplode(',', (string)$fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
154  }
155  if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
156  $connection = $connectionPool->getConnectionForTable($fieldConfig['foreign_table']);
157  // nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping
158  // the whole field as it is not known where it is used in the WHERE clause
159  $replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\'');
160  }
161  }
162  $config['addWhere'] = ‪QueryHelper::quoteDatabaseIdentifiers($connectionPool->getConnectionForTable($queryTable), strtr(' ' . $config['addWhere'], $replacement));
163  }
164 
165  // instantiate the class that should fetch the records for this $queryTable
166  $receiverClassName = $config['receiverClass'] ?? '';
167  if (!class_exists($receiverClassName)) {
168  $receiverClassName = SuggestWizardDefaultReceiver::class;
169  }
170  $receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
171  $params = [
172  'value' => $search,
173  'uid' => ‪$uid,
174  ];
175  $rows = $receiverObj->queryTable($params);
176  if (empty($rows)) {
177  continue;
178  }
179  $resultRows = $rows + $resultRows;
180  unset($rows);
181  }
182 
183  // Limit the number of items in the result list
184  $maxItems = $config['maxItemsInResultList'] ?? 10;
185  $maxItems = min(count($resultRows), $maxItems);
186 
187  array_splice($resultRows, $maxItems);
188  return new ‪JsonResponse(array_values($resultRows));
189  }
190 
196  protected function ‪isTableHidden(array $tableConfig)
197  {
198  return (bool)($tableConfig['ctrl']['hideTable'] ?? false);
199  }
200 
207  protected function ‪currentBackendUserMayAccessTable(array $tableConfig)
208  {
209  if ($this->‪getBackendUser()->isAdmin()) {
210  return true;
211  }
212 
213  // If the user is no admin, they may not access admin-only tables
214  if ($tableConfig['ctrl']['adminOnly']) {
215  return false;
216  }
217 
218  // allow access to root level pages if security restrictions should be bypassed
219  return !$tableConfig['ctrl']['rootLevel'] || $tableConfig['ctrl']['security']['ignoreRootLevelRestriction'];
220  }
221 
233  protected function ‪getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
234  {
235  $config = (array)($wizardConfig['default'] ?? []);
236 
237  if (is_array($wizardConfig[$queryTable] ?? null)) {
238  ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
239  }
240  $globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'] ?? [];
241  $currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'] ?? [];
242 
243  // merge the configurations of different "levels" to get the working configuration for this table and
244  // field (i.e., go from the most general to the most special configuration)
245  if (is_array($globalSuggestTsConfig['default.'] ?? null)) {
246  ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig['default.']);
247  }
248 
249  if (is_array($globalSuggestTsConfig[$queryTable . '.'] ?? null)) {
250  ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig[$queryTable . '.']);
251  }
252 
253  // use $table instead of $queryTable here because we overlay a config
254  // for the input-field here, not for the queried table
255  if (is_array($currentFieldSuggestTsConfig['default.'] ?? null)) {
256  ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig['default.']);
257  }
258 
259  if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'] ?? null)) {
260  ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig[$queryTable . '.']);
261  }
262 
263  return $config;
264  }
265 
272  protected function ‪getTablesToQueryFromFieldConfiguration(array $fieldConfig)
273  {
274  $queryTables = [];
275 
276  if (isset($fieldConfig['allowed'])) {
277  if ($fieldConfig['allowed'] !== '*') {
278  // list of allowed tables
279  $queryTables = ‪GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
280  } else {
281  // all tables are allowed, if the user can access them
282  foreach (‪$GLOBALS['TCA'] as $tableName => $tableConfig) {
283  if (!$this->‪isTableHidden($tableConfig) && $this->‪currentBackendUserMayAccessTable($tableConfig)) {
284  $queryTables[] = $tableName;
285  }
286  }
287  unset($tableName, $tableConfig);
288  }
289  } elseif (isset($fieldConfig['foreign_table'])) {
290  // use the foreign table
291  $queryTables = [$fieldConfig['foreign_table']];
292  }
293 
294  return $queryTables;
295  }
296 
304  protected function ‪getWhereClause(array $fieldConfig)
305  {
306  if (!isset($fieldConfig['foreign_table'], $fieldConfig['foreign_table_where'])) {
307  return '';
308  }
309 
310  // strip ORDER BY clause
311  return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
312  }
313 
315  {
316  return ‪$GLOBALS['BE_USER'];
317  }
318 }
‪TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController\getWhereClause
‪string getWhereClause(array $fieldConfig)
Definition: SuggestWizardController.php:304
‪TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController\__construct
‪__construct(private readonly FlexFormTools $flexFormTools,)
Definition: SuggestWizardController.php:40
‪TYPO3\CMS\Core\Database\Query\QueryHelper\quoteDatabaseIdentifiers
‪static quoteDatabaseIdentifiers(Connection $connection, string $sql)
Definition: QueryHelper.php:224
‪TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController\currentBackendUserMayAccessTable
‪bool currentBackendUserMayAccessTable(array $tableConfig)
Definition: SuggestWizardController.php:207
‪TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController\getTablesToQueryFromFieldConfiguration
‪array getTablesToQueryFromFieldConfiguration(array $fieldConfig)
Definition: SuggestWizardController.php:272
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Backend\Controller\Wizard
Definition: AddController.php:18
‪TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController\getBackendUser
‪getBackendUser()
Definition: SuggestWizardController.php:314
‪TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools
Definition: FlexFormTools.php:40
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:61
‪TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController\isTableHidden
‪bool isTableHidden(array $tableConfig)
Definition: SuggestWizardController.php:196
‪TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController\searchAction
‪searchAction(ServerRequestInterface $request)
Definition: SuggestWizardController.php:49
‪TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController
Definition: SuggestWizardController.php:39
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪TYPO3\CMS\Core\Http\JsonResponse
Definition: JsonResponse.php:28
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Backend\Controller\Wizard\SuggestWizardController\getConfigurationForTable
‪array getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
Definition: SuggestWizardController.php:233
‪TYPO3\CMS\Backend\Attribute\AsController
Definition: AsController.php:25
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:751
‪TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver
Definition: SuggestWizardDefaultReceiver.php:40
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:817