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