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