Passed
Push — master ( 1983cd...315991 )
by
unknown
18:02
created

LiveSearch::findByTable()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 39
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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