[TASK] Migrate DatabaseConnection->cleanIntList
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Form / Wizard / SuggestWizard.php
1 <?php
2 namespace TYPO3\CMS\Backend\Form\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\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Imaging\Icon;
21 use TYPO3\CMS\Core\Imaging\IconFactory;
22 use TYPO3\CMS\Core\Utility\ArrayUtility;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24 use TYPO3\CMS\Core\Utility\MathUtility;
25 use TYPO3\CMS\Core\Utility\StringUtility;
26 use TYPO3\CMS\Lang\LanguageService;
27
28 /**
29 * Wizard for rendering an AJAX selector for records
30 */
31 class SuggestWizard
32 {
33 /**
34 * Renders an ajax-enabled text field. Also adds required JS
35 *
36 * @param string $fieldname The fieldname in the form
37 * @param string $table The table we render this selector for
38 * @param string $field The field we render this selector for
39 * @param array $row The row which is currently edited
40 * @param array $config The TSconfig of the field
41 * @return string The HTML code for the selector
42 */
43 public function renderSuggestSelector($fieldname, $table, $field, array $row, array $config)
44 {
45 /** @var $iconFactory IconFactory */
46 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
47 $languageService = $this->getLanguageService();
48 $isFlexFormField = $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'flex';
49 if ($isFlexFormField) {
50 $fieldPattern = 'data[' . $table . '][' . $row['uid'] . '][';
51 $flexformField = str_replace($fieldPattern, '', $fieldname);
52 $flexformField = substr($flexformField, 0, -1);
53 $field = str_replace(array(']['), '|', $flexformField);
54 }
55
56 // Get minimumCharacters from TCA
57 $minChars = 0;
58 if (isset($config['fieldConf']['config']['wizards']['suggest']['default']['minimumCharacters'])) {
59 $minChars = (int)$config['fieldConf']['config']['wizards']['suggest']['default']['minimumCharacters'];
60 }
61 // Overwrite it with minimumCharacters from TSConfig (TCEFORM) if given
62 if (isset($config['fieldTSConfig']['suggest.']['default.']['minimumCharacters'])) {
63 $minChars = (int)$config['fieldTSConfig']['suggest.']['default.']['minimumCharacters'];
64 }
65 $minChars = $minChars > 0 ? $minChars : 2;
66
67 // fetch the TCA field type to hand it over to the JS class
68 $type = '';
69 if (isset($config['fieldConf']['config']['type'])) {
70 $type = $config['fieldConf']['config']['type'];
71 }
72
73 $jsRow = '';
74 if ($isFlexFormField || !MathUtility::canBeInterpretedAsInteger($row['uid'])) {
75 // Ff we have a new record, we hand that row over to JS.
76 // This way we can properly retrieve the configuration of our wizard
77 // if it is shown in a flexform
78 $jsRow = serialize($row);
79 }
80
81 $selector = '
82 <div class="autocomplete t3-form-suggest-container">
83 <div class="input-group">
84 <span class="input-group-addon">' . $iconFactory->getIcon('actions-search', Icon::SIZE_SMALL)->render() . '</span>
85 <input type="search" class="t3-form-suggest form-control"
86 placeholder="' . htmlspecialchars($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.findRecord')) . '"
87 data-fieldname="' . htmlspecialchars($fieldname) . '"
88 data-table="' . htmlspecialchars($table) . '"
89 data-field="' . htmlspecialchars($field) . '"
90 data-uid="' . htmlspecialchars($row['uid']) . '"
91 data-pid="' . (int)$row['pid'] . '"
92 data-fieldtype="' . htmlspecialchars($type) . '"
93 data-minchars="' . (int)$minChars . '"
94 data-recorddata="' . htmlspecialchars($jsRow) . '"
95 />
96 </div>
97 </div>';
98
99 return $selector;
100 }
101
102 /**
103 * Ajax handler for the "suggest" feature in FormEngine.
104 *
105 * @param ServerRequestInterface $request
106 * @param ResponseInterface $response
107 * @return ResponseInterface
108 */
109 public function searchAction(ServerRequestInterface $request, ResponseInterface $response)
110 {
111 $parsedBody = $request->getParsedBody();
112 $queryParams = $request->getQueryParams();
113
114 // Get parameters from $_GET/$_POST
115 $search = isset($parsedBody['value']) ? $parsedBody['value'] : $queryParams['value'];
116 $table = isset($parsedBody['table']) ? $parsedBody['table'] : $queryParams['table'];
117 $field = isset($parsedBody['field']) ? $parsedBody['field'] : $queryParams['field'];
118 $uid = isset($parsedBody['uid']) ? $parsedBody['uid'] : $queryParams['uid'];
119 $pageId = (int)(isset($parsedBody['pid']) ? $parsedBody['pid'] : $queryParams['pid']);
120 $newRecordRow = isset($parsedBody['newRecordRow']) ? $parsedBody['newRecordRow'] : $queryParams['newRecordRow'];
121 // If the $uid is numeric, we have an already existing element, so get the
122 // TSconfig of the page itself or the element container (for non-page elements)
123 // otherwise it's a new element, so use given id of parent page (i.e., don't modify it here)
124 if (is_numeric($uid)) {
125 $row = BackendUtility::getRecord($table, $uid);
126 if ($table === 'pages') {
127 $pageId = $uid;
128 } else {
129 $pageId = $row['pid'];
130 }
131 } else {
132 $row = unserialize($newRecordRow);
133 }
134 $TSconfig = BackendUtility::getPagesTSconfig($pageId);
135 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
136 $this->overrideFieldNameAndConfigurationForFlexform($table, $field, $row, $fieldConfig);
137
138 $wizardConfig = $fieldConfig['wizards']['suggest'];
139
140 $queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
141 $whereClause = $this->getWhereClause($fieldConfig);
142
143 $resultRows = array();
144
145 // fetch the records for each query table. A query table is a table from which records are allowed to
146 // be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
147 foreach ($queryTables as $queryTable) {
148 // if the table does not exist, skip it
149 if (!is_array($GLOBALS['TCA'][$queryTable]) || empty($GLOBALS['TCA'][$queryTable])) {
150 continue;
151 }
152
153 $config = $this->getConfigurationForTable($queryTable, $wizardConfig, $TSconfig, $table, $field);
154
155 // process addWhere
156 if (!isset($config['addWhere']) && $whereClause) {
157 $config['addWhere'] = $whereClause;
158 }
159 if (isset($config['addWhere'])) {
160 $replacement = array(
161 '###THIS_UID###' => (int)$uid,
162 '###CURRENT_PID###' => (int)$pageId
163 );
164 if (isset($TSconfig['TCEFORM.'][$table . '.'][$field . '.'])) {
165 $fieldTSconfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.'];
166 if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
167 $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
168 }
169 if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
170 $replacement['###PAGE_TSCONFIG_IDLIST###'] = implode(',', GeneralUtility::intExplode(',', $fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
171 }
172 if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
173 $replacement['###PAGE_TSCONFIG_STR###'] = $GLOBALS['TYPO3_DB']->quoteStr($fieldTSconfig['PAGE_TSCONFIG_STR'], $fieldConfig['foreign_table']);
174 }
175 }
176 $config['addWhere'] = strtr(' ' . $config['addWhere'], $replacement);
177 }
178
179 // instantiate the class that should fetch the records for this $queryTable
180 $receiverClassName = $config['receiverClass'];
181 if (!class_exists($receiverClassName)) {
182 $receiverClassName = SuggestWizardDefaultReceiver::class;
183 }
184 $receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
185 $params = array('value' => $search);
186 $rows = $receiverObj->queryTable($params);
187 if (empty($rows)) {
188 continue;
189 }
190 $resultRows = $rows + $resultRows;
191 unset($rows);
192 }
193
194 // Limit the number of items in the result list
195 $maxItems = isset($config['maxItemsInResultList']) ? $config['maxItemsInResultList'] : 10;
196 $maxItems = min(count($resultRows), $maxItems);
197
198 $listItems = $this->createListItemsFromResultRow($resultRows, $maxItems);
199
200 $response->getBody()->write(json_encode($listItems));
201 return $response;
202 }
203
204 /**
205 * Returns TRUE if a table has been marked as hidden in the configuration
206 *
207 * @param array $tableConfig
208 * @return bool
209 */
210 protected function isTableHidden(array $tableConfig)
211 {
212 return (bool)$tableConfig['ctrl']['hideTable'];
213 }
214
215 /**
216 * Checks if the current backend user is allowed to access the given table, based on the ctrl-section of the
217 * table's configuration array (TCA) entry.
218 *
219 * @param array $tableConfig
220 * @return bool
221 */
222 protected function currentBackendUserMayAccessTable(array $tableConfig)
223 {
224 if ($GLOBALS['BE_USER']->isAdmin()) {
225 return true;
226 }
227
228 // If the user is no admin, they may not access admin-only tables
229 if ($tableConfig['ctrl']['adminOnly']) {
230 return false;
231 }
232
233 // allow access to root level pages if security restrictions should be bypassed
234 return !$tableConfig['ctrl']['rootLevel'] || $tableConfig['ctrl']['security']['ignoreRootLevelRestriction'];
235 }
236
237 /**
238 * Checks if the query comes from a Flexform element and if yes, resolves the field configuration from the Flexform
239 * data structure.
240 *
241 * @param string $table
242 * @param string &$field The field identifier, either a simple table field or a Flexform field path separated with |
243 * @param array $row The row we're dealing with; optional (only required for Flexform records)
244 * @param array|NULL &$fieldConfig
245 */
246 protected function overrideFieldNameAndConfigurationForFlexform($table, &$field, array $row, &$fieldConfig)
247 {
248 // check if field is a flexform reference
249 if (strpos($field, '|') === false) {
250 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
251 } else {
252 $parts = explode('|', $field);
253 if ($GLOBALS['TCA'][$table]['columns'][$parts[0]]['config']['type'] !== 'flex') {
254 return;
255 }
256
257 $flexfieldTCAConfig = $GLOBALS['TCA'][$table]['columns'][$parts[0]]['config'];
258 // @todo: should be done via data preparation, resolveAllSheetsInDS() can be deprecated then
259 if (substr($row['uid'], 0, 3) === 'NEW') {
260 // We have to cleanup record information as they are coming from FormEngines DataProvider
261 $pointerFields = GeneralUtility::trimExplode(',', $flexfieldTCAConfig['ds_pointerField']);
262 foreach ($pointerFields as $pointerField) {
263 if (is_array($row[$pointerField])) {
264 $row[$pointerField] = $row[$pointerField][0];
265 }
266 }
267 }
268 $flexformDSArray = BackendUtility::getFlexFormDS($flexfieldTCAConfig, $row, $table, $parts[0]);
269 $flexformDSArray = GeneralUtility::resolveAllSheetsInDS($flexformDSArray);
270
271 $fieldConfig = $this->getFieldConfiguration($parts, $flexformDSArray);
272 // Flexform field name levels are separated with | instead of encapsulation in [];
273 // reverse this here to be compatible with regular field names.
274 $field = str_replace('|', '][', $field);
275 }
276 }
277
278 /**
279 * Get configuration for given field by traversing the flexform path to field
280 * given in $parts
281 *
282 * @param array $parts
283 * @param array $flexformDSArray
284 * @return array
285 */
286 protected function getFieldConfiguration(array $parts, array $flexformDSArray)
287 {
288 $relevantParts = [];
289 foreach ($parts as $part) {
290 if ($this->isRelevantFlexField($part)) {
291 $relevantParts[] = $part;
292 }
293 }
294 // throw away db field name for flexform field
295 array_shift($relevantParts);
296
297 $flexformElement = array_pop($relevantParts);
298 $sheetName = array_shift($relevantParts);
299 $flexSubDataStructure = $flexformDSArray['sheets'][$sheetName];
300 foreach ($relevantParts as $relevantPart) {
301 $flexSubDataStructure = $this->getSubConfigurationForSections($flexSubDataStructure, $relevantPart);
302 }
303 $fieldConfig = $this->getNestedDsFieldConfig($flexSubDataStructure, $flexformElement);
304 return $fieldConfig;
305 }
306
307 /**
308 * Recursively get sub sections in data structure by name
309 *
310 * @param array $dataStructure
311 * @param string $fieldName
312 * @return array
313 */
314 protected function getSubConfigurationForSections(array $dataStructure, $fieldName)
315 {
316 $fieldConfig = array();
317 $elements = $dataStructure['ROOT']['el'] ? $dataStructure['ROOT']['el'] : $dataStructure['el'];
318 if (is_array($elements)) {
319 foreach ($elements as $k => $ds) {
320 if ($k === $fieldName) {
321 $fieldConfig = $ds;
322 break;
323 } elseif (isset($ds['el'][$fieldName])) {
324 $fieldConfig = $ds['el'][$fieldName];
325 break;
326 } else {
327 $fieldConfig = $this->getSubConfigurationForSections($ds, $fieldName);
328 }
329 }
330 }
331 return $fieldConfig;
332 }
333
334 /**
335 * Search a data structure array recursively -- including within nested
336 * (repeating) elements -- for a particular field config.
337 *
338 * @param array $dataStructure The data structure
339 * @param string $fieldName The field name
340 * @return array
341 */
342 protected function getNestedDsFieldConfig(array $dataStructure, $fieldName)
343 {
344 $fieldConfig = array();
345 $elements = $dataStructure['ROOT']['el'] ? $dataStructure['ROOT']['el'] : $dataStructure['el'];
346 if (is_array($elements)) {
347 foreach ($elements as $k => $ds) {
348 if ($k === $fieldName) {
349 $fieldConfig = $ds['TCEforms']['config'];
350 break;
351 } elseif (isset($ds['el'][$fieldName]['TCEforms']['config'])) {
352 $fieldConfig = $ds['el'][$fieldName]['TCEforms']['config'];
353 break;
354 } else {
355 $fieldConfig = $this->getNestedDsFieldConfig($ds, $fieldName);
356 }
357 }
358 }
359 return $fieldConfig;
360 }
361
362 /**
363 * Checks whether the field is an actual identifier or just "array filling"
364 *
365 * @param string $fieldName
366 * @return bool
367 */
368 protected function isRelevantFlexField($fieldName)
369 {
370 return !(
371 StringUtility::beginsWith($fieldName, 'ID-') ||
372 $fieldName === 'lDEF' ||
373 $fieldName === 'vDEF' ||
374 $fieldName === 'data' ||
375 $fieldName === 'el'
376 );
377 }
378
379 /**
380 * Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the
381 * TSconfig.
382 *
383 * @param string $queryTable The table to query
384 * @param array $wizardConfig The configuration for the wizard as configured in the data structure
385 * @param array $TSconfig The TSconfig array of the current page
386 * @param string $table The table where the wizard is used
387 * @param string $field The field where the wizard is used
388 * @return array
389 */
390 protected function getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
391 {
392 $config = (array)$wizardConfig['default'];
393
394 if (is_array($wizardConfig[$queryTable])) {
395 ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
396 }
397 $globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'];
398 $currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'];
399
400 // merge the configurations of different "levels" to get the working configuration for this table and
401 // field (i.e., go from the most general to the most special configuration)
402 if (is_array($globalSuggestTsConfig['default.'])) {
403 ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig['default.']);
404 }
405
406 if (is_array($globalSuggestTsConfig[$queryTable . '.'])) {
407 ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig[$queryTable . '.']);
408 }
409
410 // use $table instead of $queryTable here because we overlay a config
411 // for the input-field here, not for the queried table
412 if (is_array($currentFieldSuggestTsConfig['default.'])) {
413 ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig['default.']);
414 }
415
416 if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'])) {
417 ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig[$queryTable . '.']);
418 }
419
420 return $config;
421 }
422
423 /**
424 * Creates a list of <li> elements from a list of results returned by the receiver.
425 *
426 * @param array $resultRows
427 * @param int $maxItems
428 * @return array
429 */
430 protected function createListItemsFromResultRow(array $resultRows, $maxItems)
431 {
432 if (empty($resultRows)) {
433 return array();
434 }
435 $listItems = array();
436
437 // traverse all found records and sort them
438 $rowsSort = array();
439 foreach ($resultRows as $key => $row) {
440 $rowsSort[$key] = $row['text'];
441 }
442 asort($rowsSort);
443 $rowsSort = array_keys($rowsSort);
444
445 // put together the selector entries
446 for ($i = 0; $i < $maxItems; ++$i) {
447 $listItems[] = $resultRows[$rowsSort[$i]];
448 }
449 return $listItems;
450 }
451
452 /**
453 * Checks the given field configuration for the tables that should be used for querying and returns them as an
454 * array.
455 *
456 * @param array $fieldConfig
457 * @return array
458 */
459 protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig)
460 {
461 $queryTables = array();
462
463 if (isset($fieldConfig['allowed'])) {
464 if ($fieldConfig['allowed'] !== '*') {
465 // list of allowed tables
466 $queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
467 } else {
468 // all tables are allowed, if the user can access them
469 foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
470 if (!$this->isTableHidden($tableConfig) && $this->currentBackendUserMayAccessTable($tableConfig)) {
471 $queryTables[] = $tableName;
472 }
473 }
474 unset($tableName, $tableConfig);
475 }
476 } elseif (isset($fieldConfig['foreign_table'])) {
477 // use the foreign table
478 $queryTables = array($fieldConfig['foreign_table']);
479 }
480
481 return $queryTables;
482 }
483
484 /**
485 * Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table
486 * is configured and should be used; it could e.g. be used to limit to a certain subset of records from the
487 * foreign table
488 *
489 * @param array $fieldConfig
490 * @return string
491 */
492 protected function getWhereClause(array $fieldConfig)
493 {
494 if (!isset($fieldConfig['foreign_table'])) {
495 return '';
496 }
497
498 // strip ORDER BY clause
499 return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
500 }
501
502 /**
503 * @return LanguageService
504 */
505 protected function getLanguageService()
506 {
507 return $GLOBALS['LANG'];
508 }
509 }