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