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