Completed
Push — master ( a088ad...55fcd8 )
by
unknown
23:17 queued 05:43
created

LiveSearch::getRecordTitlePrep()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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