4510555083fd3780d7bef18cfcabcca69583be46
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / Wizard / SuggestWizardController.php
1 <?php
2 namespace TYPO3\CMS\Backend\Controller\Wizard;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Psr\Http\Message\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
19 use TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver;
20 use TYPO3\CMS\Backend\Utility\BackendUtility;
21 use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
22 use TYPO3\CMS\Core\Database\ConnectionPool;
23 use TYPO3\CMS\Core\Http\JsonResponse;
24 use TYPO3\CMS\Core\Utility\ArrayUtility;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26
27 /**
28 * Receives ajax request from FormEngine suggest wizard and creates suggest answer as json result
29 */
30 class SuggestWizardController
31 {
32 /**
33 * Ajax handler for the "suggest" feature in FormEngine.
34 *
35 * @param ServerRequestInterface $request
36 * @throws \RuntimeException for incomplete or invalid arguments
37 * @return ResponseInterface
38 */
39 public function searchAction(ServerRequestInterface $request): ResponseInterface
40 {
41 $parsedBody = $request->getParsedBody();
42
43 $search = $parsedBody['value'];
44 $tableName = $parsedBody['tableName'];
45 $fieldName = $parsedBody['fieldName'];
46 $uid = $parsedBody['uid'];
47 $pid = (int)$parsedBody['pid'];
48 $dataStructureIdentifier = '';
49 if (!empty($parsedBody['dataStructureIdentifier'])) {
50 $dataStructureIdentifier = json_encode($parsedBody['dataStructureIdentifier']);
51 }
52 $flexFormSheetName = $parsedBody['flexFormSheetName'];
53 $flexFormFieldName = $parsedBody['flexFormFieldName'];
54 $flexFormContainerName = $parsedBody['flexFormContainerName'];
55 $flexFormContainerFieldName = $parsedBody['flexFormContainerFieldName'];
56
57 // Determine TCA config of field
58 if (empty($dataStructureIdentifier)) {
59 // Normal columns field
60 $fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
61 $fieldNameInPageTsConfig = $fieldName;
62 } else {
63 // A flex flex form field
64 $flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
65 $dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
66 if (empty($flexFormContainerFieldName)) {
67 // @todo: See if a path in pageTsConfig like "TCEForm.tableName.theContainerFieldName =" is useful and works with other pageTs, too.
68 $fieldNameInPageTsConfig = $flexFormFieldName;
69 if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
70 ['el'][$flexFormFieldName]['TCEforms']['config'])
71 ) {
72 throw new \RuntimeException(
73 'Specified path ' . $flexFormFieldName . ' not found in flex form data structure',
74 1480609491
75 );
76 }
77 $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
78 ['el'][$flexFormFieldName]['TCEforms']['config'];
79 } else {
80 $fieldNameInPageTsConfig = $flexFormContainerFieldName;
81 if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
82 ['el'][$flexFormFieldName]
83 ['el'][$flexFormContainerName]
84 ['el'][$flexFormContainerFieldName]['TCEforms']['config'])
85 ) {
86 throw new \RuntimeException(
87 'Specified path ' . $flexFormContainerName . ' not found in flex form section container data structure',
88 1480611208
89 );
90 }
91 $fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
92 ['el'][$flexFormFieldName]
93 ['el'][$flexFormContainerName]
94 ['el'][$flexFormContainerFieldName]['TCEforms']['config'];
95 }
96 }
97
98 $pageTsConfig = BackendUtility::getPagesTSconfig($pid);
99
100 $wizardConfig = $fieldConfig['suggestOptions'] ?? [];
101
102 $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
103 $whereClause = $this->getWhereClause($fieldConfig);
104
105 $resultRows = [];
106
107 // fetch the records for each query table. A query table is a table from which records are allowed to
108 // be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
109 foreach ($queryTables as $queryTable) {
110 // if the table does not exist, skip it
111 if (!is_array($GLOBALS['TCA'][$queryTable]) || empty($GLOBALS['TCA'][$queryTable])) {
112 continue;
113 }
114
115 $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $pageTsConfig, $tableName, $fieldNameInPageTsConfig);
116
117 // process addWhere
118 if (!isset($config['addWhere']) && $whereClause) {
119 $config['addWhere'] = $whereClause;
120 }
121 if (isset($config['addWhere'])) {
122 $replacement = [
123 '###THIS_UID###' => (int)$uid,
124 '###CURRENT_PID###' => (int)$pid
125 ];
126 if (isset($pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'])) {
127 $fieldTSconfig = $pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'];
128 if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
129 $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
130 }
131 if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
132 $replacement['###PAGE_TSCONFIG_IDLIST###'] = implode(',', GeneralUtility::intExplode(',', $fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
133 }
134 if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
135 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($fieldConfig['foreign_table']);
136 // nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping
137 // the whole field as it is not known where it is used in the WHERE clause
138 $replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\'');
139 }
140 }
141 $config['addWhere'] = strtr(' ' . $config['addWhere'], $replacement);
142 }
143
144 // instantiate the class that should fetch the records for this $queryTable
145 $receiverClassName = $config['receiverClass'];
146 if (!class_exists($receiverClassName)) {
147 $receiverClassName = SuggestWizardDefaultReceiver::class;
148 }
149 $receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
150 $params = ['value' => $search];
151 $rows = $receiverObj->queryTable($params);
152 if (empty($rows)) {
153 continue;
154 }
155 $resultRows = $rows + $resultRows;
156 unset($rows);
157 }
158
159 // Limit the number of items in the result list
160 $maxItems = isset($config['maxItemsInResultList']) ? $config['maxItemsInResultList'] : 10;
161 $maxItems = min(count($resultRows), $maxItems);
162
163 array_splice($resultRows, $maxItems);
164 return GeneralUtility::makeInstance(JsonResponse::class)->setPayload(array_values($resultRows));
165 }
166
167 /**
168 * Returns TRUE if a table has been marked as hidden in the configuration
169 *
170 * @param array $tableConfig
171 * @return bool
172 */
173 protected function isTableHidden(array $tableConfig)
174 {
175 return (bool)$tableConfig['ctrl']['hideTable'];
176 }
177
178 /**
179 * Checks if the current backend user is allowed to access the given table, based on the ctrl-section of the
180 * table's configuration array (TCA) entry.
181 *
182 * @param array $tableConfig
183 * @return bool
184 */
185 protected function currentBackendUserMayAccessTable(array $tableConfig)
186 {
187 if ($this->getBackendUser()->isAdmin()) {
188 return true;
189 }
190
191 // If the user is no admin, they may not access admin-only tables
192 if ($tableConfig['ctrl']['adminOnly']) {
193 return false;
194 }
195
196 // allow access to root level pages if security restrictions should be bypassed
197 return !$tableConfig['ctrl']['rootLevel'] || $tableConfig['ctrl']['security']['ignoreRootLevelRestriction'];
198 }
199
200 /**
201 * Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the
202 * TSconfig.
203 *
204 * @param string $queryTable The table to query
205 * @param array $wizardConfig The configuration for the wizard as configured in the data structure
206 * @param array $TSconfig The TSconfig array of the current page
207 * @param string $table The table where the wizard is used
208 * @param string $field The field where the wizard is used
209 * @return array
210 */
211 protected function getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
212 {
213 $config = (array)$wizardConfig['default'];
214
215 if (is_array($wizardConfig[$queryTable])) {
216 ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
217 }
218 $globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'];
219 $currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'];
220
221 // merge the configurations of different "levels" to get the working configuration for this table and
222 // field (i.e., go from the most general to the most special configuration)
223 if (is_array($globalSuggestTsConfig['default.'])) {
224 ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig['default.']);
225 }
226
227 if (is_array($globalSuggestTsConfig[$queryTable . '.'])) {
228 ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig[$queryTable . '.']);
229 }
230
231 // use $table instead of $queryTable here because we overlay a config
232 // for the input-field here, not for the queried table
233 if (is_array($currentFieldSuggestTsConfig['default.'])) {
234 ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig['default.']);
235 }
236
237 if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'])) {
238 ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig[$queryTable . '.']);
239 }
240
241 return $config;
242 }
243
244 /**
245 * Checks the given field configuration for the tables that should be used for querying and returns them as an
246 * array.
247 *
248 * @param array $fieldConfig
249 * @return array
250 */
251 protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig)
252 {
253 $queryTables = [];
254
255 if (isset($fieldConfig['allowed'])) {
256 if ($fieldConfig['allowed'] !== '*') {
257 // list of allowed tables
258 $queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
259 } else {
260 // all tables are allowed, if the user can access them
261 foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
262 if (!$this->isTableHidden($tableConfig) && $this->currentBackendUserMayAccessTable($tableConfig)) {
263 $queryTables[] = $tableName;
264 }
265 }
266 unset($tableName, $tableConfig);
267 }
268 } elseif (isset($fieldConfig['foreign_table'])) {
269 // use the foreign table
270 $queryTables = [$fieldConfig['foreign_table']];
271 }
272
273 return $queryTables;
274 }
275
276 /**
277 * Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table
278 * is configured and should be used; it could e.g. be used to limit to a certain subset of records from the
279 * foreign table
280 *
281 * @param array $fieldConfig
282 * @return string
283 */
284 protected function getWhereClause(array $fieldConfig)
285 {
286 if (!isset($fieldConfig['foreign_table'])) {
287 return '';
288 }
289
290 // strip ORDER BY clause
291 return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
292 }
293
294 /**
295 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
296 */
297 protected function getBackendUser()
298 {
299 return $GLOBALS['BE_USER'];
300 }
301 }