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