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