[BUGFIX] Remove bad PHP code in LiveSearch
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Search / LiveSearch / LiveSearch.php
1 <?php
2 namespace TYPO3\CMS\Backend\Search\LiveSearch;
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\Imaging\Icon;
19 use TYPO3\CMS\Core\Imaging\IconFactory;
20 use TYPO3\CMS\Core\Type\Bitmask\Permission;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22 use TYPO3\CMS\Core\Utility\MathUtility;
23
24 /**
25 * Class for handling backend live search.
26 */
27 class LiveSearch
28 {
29 /**
30 * @var string
31 */
32 const PAGE_JUMP_TABLE = 'pages';
33
34 /**
35 * @var int
36 */
37 const RECURSIVE_PAGE_LEVEL = 99;
38
39 /**
40 * @var int
41 */
42 const GROUP_TITLE_MAX_LENGTH = 15;
43
44 /**
45 * @var int
46 */
47 const RECORD_TITLE_MAX_LENGTH = 28;
48
49 /**
50 * @var string
51 */
52 private $queryString = '';
53
54 /**
55 * @var int
56 */
57 private $startCount = 0;
58
59 /**
60 * @var int
61 */
62 private $limitCount = 5;
63
64 /**
65 * @var string
66 */
67 protected $userPermissions = '';
68
69 /**
70 * @var \TYPO3\CMS\Backend\Search\LiveSearch\QueryParser
71 */
72 protected $queryParser = null;
73
74 /**
75 * Initialize access settings
76 */
77 public function __construct()
78 {
79 $this->userPermissions = $GLOBALS['BE_USER']->getPagePermsClause(1);
80 $this->queryParser = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Search\LiveSearch\QueryParser::class);
81 }
82
83 /**
84 * Find records from database based on the given $searchQuery.
85 *
86 * @param string $searchQuery
87 * @return array Result list of database search.
88 */
89 public function find($searchQuery)
90 {
91 $recordArray = array();
92 $pageList = array();
93 $mounts = $GLOBALS['BE_USER']->returnWebmounts();
94 foreach ($mounts as $pageId) {
95 $pageList[] = $this->getAvailablePageIds($pageId, self::RECURSIVE_PAGE_LEVEL);
96 }
97 $pageIdList = implode(',', array_unique(explode(',', implode(',', $pageList))));
98 unset($pageList);
99 $limit = $this->startCount . ',' . $this->limitCount;
100 if ($this->queryParser->isValidCommand($searchQuery)) {
101 $this->setQueryString($this->queryParser->getSearchQueryValue($searchQuery));
102 $tableName = $this->queryParser->getTableNameFromCommand($searchQuery);
103 if ($tableName) {
104 $recordArray[] = $this->findByTable($tableName, $pageIdList, $limit);
105 }
106 } else {
107 $this->setQueryString($searchQuery);
108 $recordArray = $this->findByGlobalTableList($pageIdList);
109 }
110 return $recordArray;
111 }
112
113 /**
114 * Retrieve the page record from given $id.
115 *
116 * @param int $id
117 * @return array
118 */
119 protected function findPageById($id)
120 {
121 $pageRecord = array();
122 $row = BackendUtility::getRecord(self::PAGE_JUMP_TABLE, $id);
123 if (is_array($row)) {
124 $pageRecord = $row;
125 }
126 return $pageRecord;
127 }
128
129 /**
130 * Find records from all registered TCA table & column values.
131 *
132 * @param string $pageIdList Comma separated list of page IDs
133 * @return array Records found in the database matching the searchQuery
134 */
135 protected function findByGlobalTableList($pageIdList)
136 {
137 $limit = $this->limitCount;
138 $getRecordArray = array();
139 foreach ($GLOBALS['TCA'] as $tableName => $value) {
140 // if no access for the table (read or write), skip this table
141 if (!$GLOBALS['BE_USER']->check('tables_select', $tableName) && !$GLOBALS['BE_USER']->check('tables_modify', $tableName)) {
142 continue;
143 }
144 $recordArray = $this->findByTable($tableName, $pageIdList, '0,' . $limit);
145 $recordCount = count($recordArray);
146 if ($recordCount) {
147 $limit = $limit - $recordCount;
148 $getRecordArray[] = $recordArray;
149 if ($limit <= 0) {
150 break;
151 }
152 }
153 }
154 return $getRecordArray;
155 }
156
157 /**
158 * Find records by given table name.
159 *
160 * @param string $tableName Database table name
161 * @param string $pageIdList Comma separated list of page IDs
162 * @param string $limit MySql Limit notation
163 * @return array Records found in the database matching the searchQuery
164 * @see getRecordArray()
165 * @see makeOrderByTable()
166 * @see makeQuerySearchByTable()
167 * @see extractSearchableFieldsFromTable()
168 */
169 protected function findByTable($tableName, $pageIdList, $limit)
170 {
171 $fieldsToSearchWithin = $this->extractSearchableFieldsFromTable($tableName);
172 $getRecordArray = array();
173 if (!empty($fieldsToSearchWithin)) {
174 $pageBasedPermission = $tableName == 'pages' && $this->userPermissions ? $this->userPermissions : '1=1 ';
175 $where = 'pid IN (' . $pageIdList . ') AND ' . $pageBasedPermission . $this->makeQuerySearchByTable($tableName, $fieldsToSearchWithin);
176 $getRecordArray = $this->getRecordArray($tableName, $where, $this->makeOrderByTable($tableName), $limit);
177 }
178 return $getRecordArray;
179 }
180
181 /**
182 * Process the Database operation to get the search result.
183 *
184 * @param string $tableName Database table name
185 * @param string $where
186 * @param string $orderBy
187 * @param string $limit MySql Limit notation
188 * @return array
189 * @see getTitleFromCurrentRow()
190 * @see getEditLink()
191 */
192 protected function getRecordArray($tableName, $where, $orderBy, $limit)
193 {
194 $collect = array();
195 $isFirst = true;
196 $queryParts = array(
197 'SELECT' => '*',
198 'FROM' => $tableName,
199 'WHERE' => $where,
200 'ORDERBY' => $orderBy,
201 'LIMIT' => $limit
202 );
203 $result = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($queryParts);
204 $dbCount = $GLOBALS['TYPO3_DB']->sql_num_rows($result);
205 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
206 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result)) {
207 $title = 'id=' . $row['uid'] . ', pid=' . $row['pid'];
208 $collect[] = array(
209 'id' => $tableName . ':' . $row['uid'],
210 'pageId' => $tableName === 'pages' ? $row['uid'] : $row['pid'],
211 'typeLabel' => htmlspecialchars($this->getTitleOfCurrentRecordType($tableName)),
212 'iconHTML' => '<span title="' . htmlspecialchars($title) . '">' . $iconFactory->getIconForRecord($tableName, $row, Icon::SIZE_SMALL)->render() . '</span>',
213 'title' => htmlspecialchars(BackendUtility::getRecordTitle($tableName, $row)),
214 'editLink' => htmlspecialchars($this->getEditLink($tableName, $row))
215 );
216 $isFirst = false;
217 }
218 $GLOBALS['TYPO3_DB']->sql_free_result($result);
219 return $collect;
220 }
221
222 /**
223 * Build a backend edit link based on given record.
224 *
225 * @param string $tableName Record table name
226 * @param array $row Current record row from database.
227 * @return string Link to open an edit window for record.
228 * @see \TYPO3\CMS\Backend\Utility\BackendUtility::readPageAccess()
229 */
230 protected function getEditLink($tableName, $row)
231 {
232 $pageInfo = BackendUtility::readPageAccess($row['pid'], $this->userPermissions);
233 $calcPerms = $GLOBALS['BE_USER']->calcPerms($pageInfo);
234 $editLink = '';
235 if ($tableName == 'pages') {
236 $localCalcPerms = $GLOBALS['BE_USER']->calcPerms(BackendUtility::getRecord('pages', $row['uid']));
237 $permsEdit = $localCalcPerms & Permission::PAGE_EDIT;
238 } else {
239 $permsEdit = $calcPerms & Permission::CONTENT_EDIT;
240 }
241 // "Edit" link - Only if permissions to edit the page-record of the content of the parent page ($this->id)
242 if ($permsEdit) {
243 $returnUrl = BackendUtility::getModuleUrl('web_list', array('id' => $row['pid']));
244 $editLink = BackendUtility::getModuleUrl('record_edit', array(
245 'edit[' . $tableName . '][' . $row['uid'] . ']' => 'edit',
246 'returnUrl' => $returnUrl
247 ));
248 }
249 return $editLink;
250 }
251
252 /**
253 * Retrieve the record name
254 *
255 * @param string $tableName Record table name
256 * @return string
257 */
258 protected function getTitleOfCurrentRecordType($tableName)
259 {
260 return $GLOBALS['LANG']->sL($GLOBALS['TCA'][$tableName]['ctrl']['title']);
261 }
262
263 /**
264 * Crops a title string to a limited length and if it really was cropped,
265 * wrap it in a <span title="...">|</span>,
266 * which offers a tooltip with the original title when moving mouse over it.
267 *
268 * @param string $title The title string to be cropped
269 * @param int $titleLength Crop title after this length - if not set, BE_USER->uc['titleLen'] is used
270 * @return string The processed title string, wrapped in <span title="...">|</span> if cropped
271 */
272 public function getRecordTitlePrep($title, $titleLength = 0)
273 {
274 // If $titleLength is not a valid positive integer, use BE_USER->uc['titleLen']:
275 if (!$titleLength || !MathUtility::canBeInterpretedAsInteger($titleLength) || $titleLength < 0) {
276 $titleLength = $GLOBALS['BE_USER']->uc['titleLen'];
277 }
278 return htmlspecialchars(GeneralUtility::fixed_lgd_cs($title, $titleLength));
279 }
280
281 /**
282 * Build the MySql where clause by table.
283 *
284 * @param string $tableName Record table name
285 * @param array $fieldsToSearchWithin User right based visible fields where we can search within.
286 * @return string
287 */
288 protected function makeQuerySearchByTable($tableName, array $fieldsToSearchWithin)
289 {
290 $queryPart = '';
291 $whereParts = array();
292 // If the search string is a simple integer, assemble an equality comparison
293 if (MathUtility::canBeInterpretedAsInteger($this->queryString)) {
294 foreach ($fieldsToSearchWithin as $fieldName) {
295 if ($fieldName == 'uid' || $fieldName == 'pid' || isset($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
296 $fieldConfig = &$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
297 // Assemble the search condition only if the field is an integer, or is uid or pid
298 if ($fieldName == 'uid' || $fieldName == 'pid' || $fieldConfig['type'] == 'input' && $fieldConfig['eval'] && GeneralUtility::inList($fieldConfig['eval'], 'int')) {
299 $whereParts[] = $fieldName . '=' . $this->queryString;
300 } elseif (
301 $fieldConfig['type'] == 'text' ||
302 $fieldConfig['type'] == 'flex' ||
303 ($fieldConfig['type'] == 'input' && (!$fieldConfig['eval'] ||
304 !preg_match('/date|time|int/', $fieldConfig['eval'])))) {
305 // Otherwise and if the field makes sense to be searched, assemble a like condition
306 $whereParts[] = $fieldName . ' LIKE \'%' . $this->queryString . '%\'';
307 }
308 }
309 }
310 } else {
311 $like = '\'%' . $GLOBALS['TYPO3_DB']->escapeStrForLike($GLOBALS['TYPO3_DB']->quoteStr($this->queryString, $tableName), $tableName) . '%\'';
312 foreach ($fieldsToSearchWithin as $fieldName) {
313 if (isset($GLOBALS['TCA'][$tableName]['columns'][$fieldName])) {
314 $fieldConfig = &$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
315 // Check whether search should be case-sensitive or not
316 $format = 'LOWER(%s) LIKE LOWER(%s)';
317 if (is_array($fieldConfig['search'])) {
318 if (in_array('case', $fieldConfig['search'])) {
319 $format = '%s LIKE %s';
320 }
321 // Apply additional condition, if any
322 if ($fieldConfig['search']['andWhere']) {
323 $format = '((' . $fieldConfig['search']['andWhere'] . ') AND (' . $format . '))';
324 }
325 }
326 // Assemble the search condition only if the field makes sense to be searched
327 if ($fieldConfig['type'] == 'text' || $fieldConfig['type'] == 'flex' || $fieldConfig['type'] == 'input' && (!$fieldConfig['eval'] || !preg_match('/date|time|int/', $fieldConfig['eval']))) {
328 $whereParts[] = sprintf($format, $fieldName, $like);
329 }
330 }
331 }
332 }
333 // If at least one condition was defined, create the search query
334 if (!empty($whereParts)) {
335 $queryPart = ' AND (' . implode(' OR ', $whereParts) . ')';
336 // And the relevant conditions for deleted and versioned records
337 $queryPart .= BackendUtility::deleteClause($tableName);
338 $queryPart .= BackendUtility::versioningPlaceholderClause($tableName);
339 $queryPart .= BackendUtility::getWorkspaceWhereClause($tableName);
340 } else {
341 $queryPart = ' AND 0 = 1';
342 }
343 return $queryPart;
344 }
345
346 /**
347 * Build the MySql ORDER BY statement.
348 *
349 * @param string $tableName Record table name
350 * @return string
351 */
352 protected function makeOrderByTable($tableName)
353 {
354 $orderBy = '';
355 if (is_array($GLOBALS['TCA'][$tableName]['ctrl']) && array_key_exists('sortby', $GLOBALS['TCA'][$tableName]['ctrl'])) {
356 $sortBy = trim($GLOBALS['TCA'][$tableName]['ctrl']['sortby']);
357 if (!empty($sortBy)) {
358 $orderBy = 'ORDER BY ' . $sortBy;
359 }
360 } else {
361 $orderBy = $GLOBALS['TCA'][$tableName]['ctrl']['default_sortby'];
362 }
363 return $GLOBALS['TYPO3_DB']->stripOrderBy($orderBy);
364 }
365
366 /**
367 * Get all fields from given table where we can search for.
368 *
369 * @param string $tableName Name of the table for which to get the searchable fields
370 * @return array
371 */
372 protected function extractSearchableFieldsFromTable($tableName)
373 {
374 // Get the list of fields to search in from the TCA, if any
375 if (isset($GLOBALS['TCA'][$tableName]['ctrl']['searchFields'])) {
376 $fieldListArray = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$tableName]['ctrl']['searchFields'], true);
377 } else {
378 $fieldListArray = array();
379 }
380 // Add special fields
381 if ($GLOBALS['BE_USER']->isAdmin()) {
382 $fieldListArray[] = 'uid';
383 $fieldListArray[] = 'pid';
384 }
385 return $fieldListArray;
386 }
387
388 /**
389 * Safely retrieve the queryString.
390 *
391 * @param string $tableName
392 * @return string
393 */
394 public function getQueryString($tableName = '')
395 {
396 return $GLOBALS['TYPO3_DB']->quoteStr($this->queryString, $tableName);
397 }
398
399 /**
400 * Setter for limit value.
401 *
402 * @param int $limitCount
403 * @return void
404 */
405 public function setLimitCount($limitCount)
406 {
407 $limit = MathUtility::convertToPositiveInteger($limitCount);
408 if ($limit > 0) {
409 $this->limitCount = $limit;
410 }
411 }
412
413 /**
414 * Setter for start count value.
415 *
416 * @param int $startCount
417 * @return void
418 */
419 public function setStartCount($startCount)
420 {
421 $this->startCount = MathUtility::convertToPositiveInteger($startCount);
422 }
423
424 /**
425 * Setter for the search query string.
426 *
427 * @param string $queryString
428 * @return void
429 * @see \TYPO3\CMS\Core\Utility\GeneralUtility::removeXSS()
430 */
431 public function setQueryString($queryString)
432 {
433 $this->queryString = GeneralUtility::removeXSS($queryString);
434 }
435
436 /**
437 * Creates an instance of \TYPO3\CMS\Backend\Tree\View\PageTreeView which will select a
438 * page tree to $depth and return the object. In that object we will find the ids of the tree.
439 *
440 * @param int $id Page id.
441 * @param int $depth Depth to go down.
442 * @return string Comma separated list of uids
443 */
444 protected function getAvailablePageIds($id, $depth)
445 {
446 $tree = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Tree\View\PageTreeView::class);
447 $tree->init('AND ' . $this->userPermissions);
448 $tree->makeHTML = 0;
449 $tree->fieldArray = array('uid', 'php_tree_stop');
450 if ($depth) {
451 $tree->getTree($id, $depth, '');
452 }
453 $tree->ids[] = $id;
454 // add workspace pid - workspace permissions are taken into account by where clause later
455 $tree->ids[] = -1;
456 $idList = implode(',', $tree->ids);
457 return $idList;
458 }
459 }