[!!!][TASK] Improve flex and TCA handling in FormEngine
[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\Utility\ArrayUtility;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25
26 /**
27 * Receives ajax request from FormEngine suggest wizard and creates suggest answer as json result
28 */
29 class SuggestWizardController
30 {
31 /**
32 * Ajax handler for the "suggest" feature in FormEngine.
33 *
34 * @param ServerRequestInterface $request
35 * @param ResponseInterface $response
36 * @throws \RuntimeException for incomplete or invalid arguments
37 * @return ResponseInterface
38 */
39 public function searchAction(ServerRequestInterface $request, ResponseInterface $response)
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['wizards']['suggest'];
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
165 $response->getBody()->write(json_encode(array_values($resultRows)));
166 return $response;
167 }
168
169 /**
170 * Returns TRUE if a table has been marked as hidden in the configuration
171 *
172 * @param array $tableConfig
173 * @return bool
174 */
175 protected function isTableHidden(array $tableConfig)
176 {
177 return (bool)$tableConfig['ctrl']['hideTable'];
178 }
179
180 /**
181 * Checks if the current backend user is allowed to access the given table, based on the ctrl-section of the
182 * table's configuration array (TCA) entry.
183 *
184 * @param array $tableConfig
185 * @return bool
186 */
187 protected function currentBackendUserMayAccessTable(array $tableConfig)
188 {
189 if ($this->getBackendUser()->isAdmin()) {
190 return true;
191 }
192
193 // If the user is no admin, they may not access admin-only tables
194 if ($tableConfig['ctrl']['adminOnly']) {
195 return false;
196 }
197
198 // allow access to root level pages if security restrictions should be bypassed
199 return !$tableConfig['ctrl']['rootLevel'] || $tableConfig['ctrl']['security']['ignoreRootLevelRestriction'];
200 }
201
202 /**
203 * Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the
204 * TSconfig.
205 *
206 * @param string $queryTable The table to query
207 * @param array $wizardConfig The configuration for the wizard as configured in the data structure
208 * @param array $TSconfig The TSconfig array of the current page
209 * @param string $table The table where the wizard is used
210 * @param string $field The field where the wizard is used
211 * @return array
212 */
213 protected function getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
214 {
215 $config = (array)$wizardConfig['default'];
216
217 if (is_array($wizardConfig[$queryTable])) {
218 ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
219 }
220 $globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'];
221 $currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'];
222
223 // merge the configurations of different "levels" to get the working configuration for this table and
224 // field (i.e., go from the most general to the most special configuration)
225 if (is_array($globalSuggestTsConfig['default.'])) {
226 ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig['default.']);
227 }
228
229 if (is_array($globalSuggestTsConfig[$queryTable . '.'])) {
230 ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig[$queryTable . '.']);
231 }
232
233 // use $table instead of $queryTable here because we overlay a config
234 // for the input-field here, not for the queried table
235 if (is_array($currentFieldSuggestTsConfig['default.'])) {
236 ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig['default.']);
237 }
238
239 if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'])) {
240 ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig[$queryTable . '.']);
241 }
242
243 return $config;
244 }
245
246 /**
247 * Checks the given field configuration for the tables that should be used for querying and returns them as an
248 * array.
249 *
250 * @param array $fieldConfig
251 * @return array
252 */
253 protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig)
254 {
255 $queryTables = [];
256
257 if (isset($fieldConfig['allowed'])) {
258 if ($fieldConfig['allowed'] !== '*') {
259 // list of allowed tables
260 $queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
261 } else {
262 // all tables are allowed, if the user can access them
263 foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
264 if (!$this->isTableHidden($tableConfig) && $this->currentBackendUserMayAccessTable($tableConfig)) {
265 $queryTables[] = $tableName;
266 }
267 }
268 unset($tableName, $tableConfig);
269 }
270 } elseif (isset($fieldConfig['foreign_table'])) {
271 // use the foreign table
272 $queryTables = [$fieldConfig['foreign_table']];
273 }
274
275 return $queryTables;
276 }
277
278 /**
279 * Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table
280 * is configured and should be used; it could e.g. be used to limit to a certain subset of records from the
281 * foreign table
282 *
283 * @param array $fieldConfig
284 * @return string
285 */
286 protected function getWhereClause(array $fieldConfig)
287 {
288 if (!isset($fieldConfig['foreign_table'])) {
289 return '';
290 }
291
292 // strip ORDER BY clause
293 return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
294 }
295
296 /**
297 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
298 */
299 protected function getBackendUser()
300 {
301 return $GLOBALS['BE_USER'];
302 }
303 }