[CLEANUP] Replace count with empty in EXT:backend
[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\GeneralUtility;
19 use TYPO3\CMS\Core\Utility\MathUtility;
20 use TYPO3\CMS\Lang\LanguageService;
21 use TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver;
22
23 /**
24 * Wizard for rendering an AJAX selector for records
25 *
26 * @author Andreas Wolf <andreas.wolf@ikt-werk.de>
27 * @author Benjamin Mack <benni@typo3.org>
28 */
29 class SuggestWizard {
30
31 /**
32 * @var string
33 */
34 protected $cssClass = 'typo3-TCEforms-suggest';
35
36 /**
37 * Renders an ajax-enabled text field. Also adds required JS
38 *
39 * @param string $fieldname The fieldname in the form
40 * @param string $table The table we render this selector for
41 * @param string $field The field we render this selector for
42 * @param array $row The row which is currently edited
43 * @param array $config The TSconfig of the field
44 * @return string The HTML code for the selector
45 */
46 public function renderSuggestSelector($fieldname, $table, $field, array $row, array $config) {
47 $languageService = $this->getLanguageService();
48 $containerCssClass = $this->cssClass . ' ' . $this->cssClass . '-position-right';
49 $suggestId = 'suggest-' . $table . '-' . $field . '-' . $row['uid'];
50 $isFlexFormField = $GLOBALS['TCA'][$table]['columns'][$field]['config']['type'] === 'flex';
51 if ($isFlexFormField) {
52 $fieldPattern = 'data[' . $table . '][' . $row['uid'] . '][';
53 $flexformField = str_replace($fieldPattern, '', $fieldname);
54 $flexformField = substr($flexformField, 0, -1);
55 $field = str_replace(array(']['), '|', $flexformField);
56 }
57 $selector = '
58 <div class="' . $containerCssClass . '" id="' . $suggestId . '">
59 <div class="input-group">
60 <span class="input-group-addon"><i class="fa fa-search"></i></span>
61 <input type="search" id="' . htmlspecialchars($fieldname) . 'Suggest" value="' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.findRecord') . '" class="form-control ' . $this->cssClass . '-search" />
62 <div class="' . $this->cssClass . '-indicator" style="display: none;" id="' . htmlspecialchars($fieldname) . 'SuggestIndicator">
63 <img src="' . $GLOBALS['BACK_PATH'] . 'gfx/spinner.gif" alt="' . $languageService->sL('LLL:EXT:lang/locallang_core.xlf:alttext.suggestSearching') . '" />
64 </div>
65 <div class="' . $this->cssClass . '-choices" style="display: none;" id="' . htmlspecialchars($fieldname) . 'SuggestChoices"></div>
66 </div>
67 </div>';
68 // Get minimumCharacters from TCA
69 $minChars = 0;
70 if (isset($config['fieldConf']['config']['wizards']['suggest']['default']['minimumCharacters'])) {
71 $minChars = (int)$config['fieldConf']['config']['wizards']['suggest']['default']['minimumCharacters'];
72 }
73 // Overwrite it with minimumCharacters from TSConfig (TCEFORM) if given
74 if (isset($config['fieldTSConfig']['suggest.']['default.']['minimumCharacters'])) {
75 $minChars = (int)$config['fieldTSConfig']['suggest.']['default.']['minimumCharacters'];
76 }
77 $minChars = $minChars > 0 ? $minChars : 2;
78
79 // fetch the TCA field type to hand it over to the JS class
80 $type = '';
81 if (isset($config['fieldConf']['config']['type'])) {
82 $type = $config['fieldConf']['config']['type'];
83 }
84
85 $jsRow = '';
86 if ($isFlexFormField && !MathUtility::canBeInterpretedAsInteger($row['uid'])) {
87 // Ff we have a new record, we hand that row over to JS.
88 // This way we can properly retrieve the configuration of our wizard
89 // if it is shown in a flexform
90 $jsRow = serialize($row);
91 }
92
93 // Replace "-" with ucwords for the JS object name
94 $jsObj = str_replace(' ', '', ucwords(str_replace(array('-', '.'), ' ', GeneralUtility::strtolower($suggestId))));
95 $selector .=
96 '<script type="text/javascript">' . LF .
97 'var ' . $jsObj . ' = new TCEForms.Suggest("' . $fieldname . '", "' . $table . '", "' . $field . '", "' . $row['uid'] . '", ' . $row['pid'] . ', ' . $minChars . ', "' . $type . '", ' . GeneralUtility::quoteJSvalue($jsRow) . ');' . LF .
98 $jsObj . '.defaultValue = "' . GeneralUtility::slashJS($languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.findRecord')) . '";' . LF .
99 '</script>' . LF;
100
101 return $selector;
102 }
103
104 /**
105 * Search a data structure array recursively -- including within nested
106 * (repeating) elements -- for a particular field config.
107 *
108 * @param array $dataStructure The data structure
109 * @param string $fieldName The field name
110 * @return array
111 */
112 protected function getNestedDsFieldConfig(array $dataStructure, $fieldName) {
113 $fieldConfig = array();
114 $elements = $dataStructure['ROOT']['el'] ? $dataStructure['ROOT']['el'] : $dataStructure['el'];
115 if (is_array($elements)) {
116 foreach ($elements as $k => $ds) {
117 if ($k === $fieldName) {
118 $fieldConfig = $ds['TCEforms']['config'];
119 break;
120 } elseif (isset($ds['el'][$fieldName]['TCEforms']['config'])) {
121 $fieldConfig = $ds['el'][$fieldName]['TCEforms']['config'];
122 break;
123 } else {
124 $fieldConfig = $this->getNestedDsFieldConfig($ds, $fieldName);
125 }
126 }
127 }
128 return $fieldConfig;
129 }
130
131 /**
132 * Ajax handler for the "suggest" feature in TCEforms.
133 *
134 * @param array $params The parameters from the AJAX call
135 * @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxObj The AJAX object representing the AJAX call
136 * @return void
137 */
138 public function processAjaxRequest($params, &$ajaxObj) {
139 // Get parameters from $_GET/$_POST
140 $search = GeneralUtility::_GP('value');
141 $table = GeneralUtility::_GP('table');
142 $field = GeneralUtility::_GP('field');
143 $uid = GeneralUtility::_GP('uid');
144 $pageId = GeneralUtility::_GP('pid');
145 $newRecordRow = GeneralUtility::_GP('newRecordRow');
146 // If the $uid is numeric, we have an already existing element, so get the
147 // TSconfig of the page itself or the element container (for non-page elements)
148 // otherwise it's a new element, so use given id of parent page (i.e., don't modify it here)
149 $row = NULL;
150 if (is_numeric($uid)) {
151 $row = BackendUtility::getRecord($table, $uid);
152 if ($table == 'pages') {
153 $pageId = $uid;
154 } else {
155 $pageId = $row['pid'];
156 }
157 } else {
158 $row = unserialize($newRecordRow);
159 }
160 $TSconfig = BackendUtility::getPagesTSconfig($pageId);
161 $queryTables = array();
162 $foreign_table_where = '';
163 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
164 $parts = explode('|', $field);
165 if ($GLOBALS['TCA'][$table]['columns'][$parts[0]]['config']['type'] === 'flex') {
166 $flexfieldTCAConfig = $GLOBALS['TCA'][$table]['columns'][$parts[0]]['config'];
167 $flexformDSArray = BackendUtility::getFlexFormDS($flexfieldTCAConfig, $row, $table, $parts[0]);
168 $flexformDSArray = GeneralUtility::resolveAllSheetsInDS($flexformDSArray);
169 $flexformElement = $parts[count($parts) - 2];
170 $continue = TRUE;
171 foreach ($flexformDSArray as $sheet) {
172 foreach ($sheet as $dataStructure) {
173 $fieldConfig = $this->getNestedDsFieldConfig($dataStructure, $flexformElement);
174 if (!empty($fieldConfig)) {
175 $continue = FALSE;
176 break;
177 }
178 }
179 if (!$continue) {
180 break;
181 }
182 }
183 $field = str_replace('|', '][', $field);
184 }
185 $wizardConfig = $fieldConfig['wizards']['suggest'];
186 if (isset($fieldConfig['allowed'])) {
187 if ($fieldConfig['allowed'] === '*') {
188 foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
189 // @todo Refactor function to BackendUtility
190 if (empty($tableConfig['ctrl']['hideTable'])
191 && ($GLOBALS['BE_USER']->isAdmin()
192 || (empty($tableConfig['ctrl']['adminOnly'])
193 && (empty($tableConfig['ctrl']['rootLevel'])
194 || !empty($tableConfig['ctrl']['security']['ignoreRootLevelRestriction']))))
195 ) {
196 $queryTables[] = $tableName;
197 }
198 }
199 unset($tableName, $tableConfig);
200 } else {
201 $queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
202 }
203 } elseif (isset($fieldConfig['foreign_table'])) {
204 $queryTables = array($fieldConfig['foreign_table']);
205 $foreign_table_where = $fieldConfig['foreign_table_where'];
206 // strip ORDER BY clause
207 $foreign_table_where = trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $foreign_table_where));
208 }
209 $resultRows = array();
210 // fetch the records for each query table. A query table is a table from which records are allowed to
211 // be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
212 foreach ($queryTables as $queryTable) {
213 // if the table does not exist, skip it
214 if (!is_array($GLOBALS['TCA'][$queryTable]) || empty($GLOBALS['TCA'][$queryTable])) {
215 continue;
216 }
217 $config = (array)$wizardConfig['default'];
218 if (is_array($wizardConfig[$queryTable])) {
219 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
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($TSconfig['TCEFORM.']['suggest.']['default.'])) {
224 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($config, $TSconfig['TCEFORM.']['suggest.']['default.']);
225 }
226 if (is_array($TSconfig['TCEFORM.']['suggest.'][$queryTable . '.'])) {
227 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($config, $TSconfig['TCEFORM.']['suggest.'][$queryTable . '.']);
228 }
229 // use $table instead of $queryTable here because we overlay a config
230 // for the input-field here, not for the queried table
231 if (is_array($TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.']['default.'])) {
232 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($config, $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.']['default.']);
233 }
234 if (is_array($TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'][$queryTable . '.'])) {
235 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($config, $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'][$queryTable . '.']);
236 }
237 //process addWhere
238 if (!isset($config['addWhere']) && $foreign_table_where) {
239 $config['addWhere'] = $foreign_table_where;
240 }
241 if (isset($config['addWhere'])) {
242 $replacement = array(
243 '###THIS_UID###' => (int)$uid,
244 '###CURRENT_PID###' => (int)$pageId
245 );
246 if (isset($TSconfig['TCEFORM.'][$table . '.'][$field . '.'])) {
247 $fieldTSconfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.'];
248 if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
249 $replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
250 }
251 if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
252 $replacement['###PAGE_TSCONFIG_IDLIST###'] = $GLOBALS['TYPO3_DB']->cleanIntList($fieldTSconfig['PAGE_TSCONFIG_IDLIST']);
253 }
254 if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
255 $replacement['###PAGE_TSCONFIG_STR###'] = $GLOBALS['TYPO3_DB']->quoteStr($fieldTSconfig['PAGE_TSCONFIG_STR'], $fieldConfig['foreign_table']);
256 }
257 }
258 $config['addWhere'] = strtr(' ' . $config['addWhere'], $replacement);
259 }
260 // instantiate the class that should fetch the records for this $queryTable
261 $receiverClassName = $config['receiverClass'];
262 if (!class_exists($receiverClassName)) {
263 $receiverClassName = SuggestWizardDefaultReceiver::class;
264 }
265 $receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
266 $params = array('value' => $search);
267 $rows = $receiverObj->queryTable($params);
268 if (empty($rows)) {
269 continue;
270 }
271 $resultRows = $rows + $resultRows;
272 unset($rows);
273 }
274 $listItems = array();
275 if (!empty($resultRows)) {
276 // traverse all found records and sort them
277 $rowsSort = array();
278 foreach ($resultRows as $key => $row) {
279 $rowsSort[$key] = $row['text'];
280 }
281 asort($rowsSort);
282 $rowsSort = array_keys($rowsSort);
283 // Limit the number of items in the result list
284 $maxItems = $config['maxItemsInResultList'] ?: 10;
285 $maxItems = min(count($resultRows), $maxItems);
286 // put together the selector entry
287 for ($i = 0; $i < $maxItems; $i++) {
288 $row = $resultRows[$rowsSort[$i]];
289 $rowId = $row['table'] . '-' . $row['uid'] . '-' . $table . '-' . $uid . '-' . $field;
290 $listItems[] = '<li' . ($row['class'] != '' ? ' class="' . $row['class'] . '"' : '') . ' id="' . $rowId . '"' . ($row['style'] != '' ? ' style="' . $row['style'] . '"' : '') . '>' . $row['sprite'] . $row['text'] . '</li>';
291 }
292 }
293 if (!empty($listItems)) {
294 $list = implode('', $listItems);
295 } else {
296 $list = '<li class="suggest-noresults"><i>' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.noRecordFound') . '</i></li>';
297 }
298 $list = '<ul class="' . $this->cssClass . '-resultlist">' . $list . '</ul>';
299 $ajaxObj->addContent(0, $list);
300 }
301
302 /**
303 * @return LanguageService
304 */
305 protected function getLanguageService() {
306 return $GLOBALS['LANG'];
307 }
308
309 }