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