Test Failed
Branch master (137376)
by Tymoteusz
20:39
created

IndexSearchRepository   F

Complexity

Total Complexity 128

Size/Duplication

Total Lines 1299
Duplicated Lines 7.7 %

Importance

Changes 0
Metric Value
dl 100
loc 1299
rs 0.5217
c 0
b 0
f 0
wmc 128

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getDescendingSortOrderFlag() 0 7 3
D execFinalQuery() 0 167 15
B checkResume() 37 50 6
A getJoinPagesForQuery() 0 3 1
A hookRequest() 11 11 3
A getTimeTracker() 0 3 1
A getResultRows_SQLpointerMysqlFulltext() 0 23 3
A multiplePagesType() 0 5 2
A getSearchType() 0 3 1
A getTypoScriptFrontendController() 0 3 1
B execFinalQuery_fulltext() 0 117 6
C freeIndexUidWhere() 27 71 8
A searchWord() 0 10 1
A getSearchRootPageIdList() 0 3 1
C initialize() 0 26 7
A languageWhere() 0 12 2
C getSearchString() 0 72 12
B mediaTypeWhere() 0 24 6
D doSearch() 6 95 17
C sectionTableWhere() 0 56 11
A getResultRows_SQLpointer() 0 14 2
A searchSentence() 0 21 1
A isTableUsed() 0 3 1
A execPHashListQuery() 0 17 1
A searchMetaphone() 8 8 1
A searchDistinct() 8 8 1
A md5inthash() 0 3 1
D getPhashList() 0 87 13

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like IndexSearchRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use IndexSearchRepository, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace TYPO3\CMS\IndexedSearch\Domain\Repository;
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 Doctrine\DBAL\Driver\Statement;
18
use TYPO3\CMS\Core\Database\Connection;
19
use TYPO3\CMS\Core\Database\ConnectionPool;
20
use TYPO3\CMS\Core\Database\Query\QueryHelper;
21
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
22
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
23
use TYPO3\CMS\Core\Utility\GeneralUtility;
24
use TYPO3\CMS\Core\Utility\MathUtility;
25
use TYPO3\CMS\IndexedSearch\Indexer;
26
use TYPO3\CMS\IndexedSearch\Utility;
27
28
/**
29
 * Index search abstraction to search through the index
30
 */
31
class IndexSearchRepository
32
{
33
    /**
34
     * Indexer object
35
     *
36
     * @var Indexer
37
     */
38
    protected $indexerObj;
39
40
    /**
41
     * External Parsers
42
     *
43
     * @var array
44
     */
45
    protected $externalParsers = [];
46
47
    /**
48
     * Frontend User Group List
49
     *
50
     * @var string
51
     */
52
    protected $frontendUserGroupList = '';
53
54
    /**
55
     * Sections
56
     * formally known as $this->piVars['sections']
57
     *
58
     * @var string
59
     */
60
    protected $sections = null;
61
62
    /**
63
     * Search type
64
     * formally known as $this->piVars['type']
65
     *
66
     * @var string
67
     */
68
    protected $searchType = null;
69
70
    /**
71
     * Language uid
72
     * formally known as $this->piVars['lang']
73
     *
74
     * @var int
75
     */
76
    protected $languageUid = null;
77
78
    /**
79
     * Media type
80
     * formally known as $this->piVars['media']
81
     *
82
     * @var int
83
     */
84
    protected $mediaType = null;
85
86
    /**
87
     * Sort order
88
     * formally known as $this->piVars['sort_order']
89
     *
90
     * @var string
91
     */
92
    protected $sortOrder = null;
93
94
    /**
95
     * Descending sort order flag
96
     * formally known as $this->piVars['desc']
97
     *
98
     * @var bool
99
     */
100
    protected $descendingSortOrderFlag = null;
101
102
    /**
103
     * Result page pointer
104
     * formally known as $this->piVars['pointer']
105
     *
106
     * @var int
107
     */
108
    protected $resultpagePointer = 0;
109
110
    /**
111
     * Number of results
112
     * formally known as $this->piVars['result']
113
     *
114
     * @var int
115
     */
116
    protected $numberOfResults = 10;
117
118
    /**
119
     * list of all root pages that will be used
120
     * If this value is set to less than zero (eg. -1) searching will happen
121
     * in ALL of the page tree with no regard to branches at all.
122
     *
123
     * @var string
124
     */
125
    protected $searchRootPageIdList;
126
127
    /**
128
     * formally known as $conf['search.']['searchSkipExtendToSubpagesChecking']
129
     * enabled through settings.searchSkipExtendToSubpagesChecking
130
     *
131
     * @var bool
132
     */
133
    protected $joinPagesForQuery = false;
134
135
    /**
136
     * Select clauses for individual words, will be filled during the search
137
     *
138
     * @var array
139
     */
140
    protected $wSelClauses = [];
141
142
    /**
143
     * Flag for exact search count
144
     * formally known as $conf['search.']['exactCount']
145
     *
146
     * Continue counting and checking of results even if we are sure
147
     * they are not displayed in this request. This will slow down your
148
     * page rendering, but it allows precise search result counters.
149
     * enabled through settings.exactCount
150
     *
151
     * @var bool
152
     */
153
    protected $useExactCount = false;
154
155
    /**
156
     * Display forbidden records
157
     * formally known as $this->conf['show.']['forbiddenRecords']
158
     *
159
     * enabled through settings.displayForbiddenRecords
160
     *
161
     * @var bool
162
     */
163
    protected $displayForbiddenRecords = false;
164
165
    /**
166
     * initialize all options that are necessary for the search
167
     *
168
     * @param array $settings the extbase plugin settings
169
     * @param array $searchData the search data
170
     * @param array $externalParsers
171
     * @param string $searchRootPageIdList
172
     */
173
    public function initialize($settings, $searchData, $externalParsers, $searchRootPageIdList)
174
    {
175
        // Initialize the indexer-class - just to use a few function (for making hashes)
176
        $this->indexerObj = GeneralUtility::makeInstance(Indexer::class);
177
        $this->externalParsers = $externalParsers;
178
        $this->searchRootPageIdList = $searchRootPageIdList;
179
        $this->frontendUserGroupList = $this->getTypoScriptFrontendController()->gr_list;
180
        // Should we use joinPagesForQuery instead of long lists of uids?
181
        if ($settings['searchSkipExtendToSubpagesChecking']) {
182
            $this->joinPagesForQuery = 1;
0 ignored issues
show
Documentation Bug introduced by
The property $joinPagesForQuery was declared of type boolean, but 1 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
183
        }
184
        if ($settings['exactCount']) {
185
            $this->useExactCount = true;
186
        }
187
        if ($settings['displayForbiddenRecords']) {
188
            $this->displayForbiddenRecords = true;
189
        }
190
        $this->sections = $searchData['sections'];
191
        $this->searchType = $searchData['searchType'];
192
        $this->languageUid = $searchData['languageUid'];
193
        $this->mediaType = isset($searchData['mediaType']) ? $searchData['mediaType'] : false;
0 ignored issues
show
Documentation Bug introduced by
It seems like IssetNode ? $searchData['mediaType'] : false can also be of type false. However, the property $mediaType is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
194
        $this->sortOrder = $searchData['sortOrder'];
195
        $this->descendingSortOrderFlag = $searchData['desc'];
196
        $this->resultpagePointer = $searchData['pointer'];
197
        if (isset($searchData['numberOfResults']) && is_numeric($searchData['numberOfResults'])) {
198
            $this->numberOfResults = (int)$searchData['numberOfResults'];
199
        }
200
    }
201
202
    /**
203
     * Get search result rows / data from database. Returned as data in array.
204
     *
205
     * @param array $searchWords Search word array
206
     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
207
     * @return bool|array FALSE if no result, otherwise an array with keys for first row, result rows and total number of results found.
208
     */
209
    public function doSearch($searchWords, $freeIndexUid = -1)
210
    {
211
        // unserializing the configuration so we can use it here:
212
        $extConf = [];
213 View Code Duplication
        if (isset($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['indexed_search'])) {
214
            $extConf = unserialize(
215
                $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['indexed_search'],
216
                ['allowed_classes' => false]
217
            );
218
        }
219
220
        // Getting SQL result pointer:
221
        $this->getTimeTracker()->push('Searching result');
222
        if ($hookObj = &$this->hookRequest('getResultRows_SQLpointer')) {
223
            $result = $hookObj->getResultRows_SQLpointer($searchWords, $freeIndexUid);
224
        } elseif (isset($extConf['useMysqlFulltext']) && $extConf['useMysqlFulltext'] === '1') {
225
            $result = $this->getResultRows_SQLpointerMysqlFulltext($searchWords, $freeIndexUid);
226
        } else {
227
            $result = $this->getResultRows_SQLpointer($searchWords, $freeIndexUid);
228
        }
229
        $this->getTimeTracker()->pull();
230
        // Organize and process result:
231
        if ($result) {
232
            // Total search-result count
233
            $count = $result->rowCount();
0 ignored issues
show
Bug introduced by
The method rowCount() does not exist on mysqli_result. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

233
            /** @scrutinizer ignore-call */ 
234
            $count = $result->rowCount();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
234
            // The pointer is set to the result page that is currently being viewed
235
            $pointer = MathUtility::forceIntegerInRange($this->resultpagePointer, 0, floor($count / $this->numberOfResults));
0 ignored issues
show
Bug introduced by
floor($count / $this->numberOfResults) of type double is incompatible with the type integer expected by parameter $max of TYPO3\CMS\Core\Utility\M...::forceIntegerInRange(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

235
            $pointer = MathUtility::forceIntegerInRange($this->resultpagePointer, 0, /** @scrutinizer ignore-type */ floor($count / $this->numberOfResults));
Loading history...
236
            // Initialize result accumulation variables:
237
            $c = 0;
238
            // Result pointer: Counts up the position in the current search-result
239
            $grouping_phashes = [];
240
            // Used to filter out duplicates.
241
            $grouping_chashes = [];
242
            // Used to filter out duplicates BASED ON cHash.
243
            $firstRow = [];
244
            // Will hold the first row in result - used to calculate relative hit-ratings.
245
            $resultRows = [];
246
            // Will hold the results rows for display.
247
            // Now, traverse result and put the rows to be displayed into an array
248
            // Each row should contain the fields from 'ISEC.*, IP.*' combined
249
            // + artificial fields "show_resume" (bool) and "result_number" (counter)
250
            while ($row = $result->fetch()) {
0 ignored issues
show
Bug introduced by
The method fetch() does not exist on mysqli_result. Did you maybe mean fetch_row()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

250
            while ($row = $result->/** @scrutinizer ignore-call */ fetch()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
251
                // Set first row
252
                if (!$c) {
253
                    $firstRow = $row;
254
                }
255
                // Tells whether we can link directly to a document
256
                // or not (depends on possible right problems)
257
                $row['show_resume'] = $this->checkResume($row);
258
                $phashGr = !in_array($row['phash_grouping'], $grouping_phashes);
259
                $chashGr = !in_array(($row['contentHash'] . '.' . $row['data_page_id']), $grouping_chashes);
260
                if ($phashGr && $chashGr) {
261
                    // Only if the resume may be shown are we going to filter out duplicates...
262
                    if ($row['show_resume'] || $this->displayForbiddenRecords) {
263
                        // Only on documents which are not multiple pages documents
264
                        if (!$this->multiplePagesType($row['item_type'])) {
265
                            $grouping_phashes[] = $row['phash_grouping'];
266
                        }
267
                        $grouping_chashes[] = $row['contentHash'] . '.' . $row['data_page_id'];
268
                        // Increase the result pointer
269
                        $c++;
270
                        // All rows for display is put into resultRows[]
271
                        if ($c > $pointer * $this->numberOfResults && $c <= $pointer * $this->numberOfResults + $this->numberOfResults) {
272
                            $row['result_number'] = $c;
273
                            $resultRows[] = $row;
274
                            // This may lead to a problem: If the result check is not stopped here, the search will take longer.
275
                            // However the result counter will not filter out grouped cHashes/pHashes that were not processed yet.
276
                            // You can change this behavior using the "search.exactCount" property (see above).
277
                            if (!$this->useExactCount && $c + 1 > ($pointer + 1) * $this->numberOfResults) {
278
                                break;
279
                            }
280
                        }
281
                    } else {
282
                        // Skip this row if the user cannot
283
                        // view it (missing permission)
284
                        $count--;
285
                    }
286
                } else {
287
                    // For each time a phash_grouping document is found
288
                    // (which is thus not displayed) the search-result count is reduced,
289
                    // so that it matches the number of rows displayed.
290
                    $count--;
291
                }
292
            }
293
294
            $result->closeCursor();
0 ignored issues
show
Bug introduced by
The method closeCursor() does not exist on mysqli_result. Did you maybe mean close()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

294
            $result->/** @scrutinizer ignore-call */ 
295
                     closeCursor();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
295
296
            return [
297
                'resultRows' => $resultRows,
298
                'firstRow' => $firstRow,
299
                'count' => $count
300
            ];
301
        }
302
        // No results found
303
        return false;
304
    }
305
306
    /**
307
     * Gets a SQL result pointer to traverse for the search records.
308
     *
309
     * @param array $searchWords Search words
310
     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
311
     * @return Statement
312
     */
313
    protected function getResultRows_SQLpointer($searchWords, $freeIndexUid = -1)
314
    {
315
        // This SEARCHES for the searchwords in $searchWords AND returns a
316
        // COMPLETE list of phash-integers of the matches.
317
        $list = $this->getPhashList($searchWords);
318
        // Perform SQL Search / collection of result rows array:
319
        if ($list) {
320
            // Do the search:
321
            $this->getTimeTracker()->push('execFinalQuery');
322
            $res = $this->execFinalQuery($list, $freeIndexUid);
323
            $this->getTimeTracker()->pull();
324
            return $res;
325
        }
326
        return false;
327
    }
328
329
    /**
330
     * Gets a SQL result pointer to traverse for the search records.
331
     *
332
     * mysql fulltext specific version triggered by ext_conf_template setting 'useMysqlFulltext'
333
     *
334
     * @param array $searchWordsArray Search words
335
     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
336
     * @return bool|\mysqli_result|object MySQLi result object / DBAL object
337
     */
338
    protected function getResultRows_SQLpointerMysqlFulltext($searchWordsArray, $freeIndexUid = -1)
339
    {
340
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('index_fulltext');
341
        if (strpos($connection->getServerVersion(), 'MySQL') !== 0) {
342
            throw new \RuntimeException(
343
                'Extension indexed_search is configured to use mysql fulltext, but table \'index_fulltext\''
344
                . ' is running on a different DBMS.',
345
                1472585525
346
            );
347
        }
348
        // Build the search string, detect which fulltext index to use, and decide whether boolean search is needed or not
349
        $searchData = $this->getSearchString($searchWordsArray);
350
        // Perform SQL Search / collection of result rows array:
351
        $resource = false;
352
        if ($searchData) {
353
            /** @var TimeTracker $timeTracker */
354
            $timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
355
            // Do the search:
356
            $timeTracker->push('execFinalQuery');
357
            $resource = $this->execFinalQuery_fulltext($searchData, $freeIndexUid);
0 ignored issues
show
Bug introduced by
$searchData of type string is incompatible with the type array expected by parameter $searchData of TYPO3\CMS\IndexedSearch\...ecFinalQuery_fulltext(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

357
            $resource = $this->execFinalQuery_fulltext(/** @scrutinizer ignore-type */ $searchData, $freeIndexUid);
Loading history...
358
            $timeTracker->pull();
359
        }
360
        return $resource;
361
    }
362
363
    /**
364
     * Returns a search string for use with MySQL FULLTEXT query
365
     *
366
     * mysql fulltext specific helper method
367
     *
368
     * @param array $searchWordArray Search word array
369
     * @return string Search string
370
     */
371
    protected function getSearchString($searchWordArray)
372
    {
373
        // Initialize variables:
374
        $count = 0;
375
        // Change this to TRUE to force BOOLEAN SEARCH MODE (useful if fulltext index is still empty)
376
        $searchBoolean = false;
377
        $fulltextIndex = 'index_fulltext.fulltextdata';
378
        // This holds the result if the search is natural (doesn't contain any boolean operators)
379
        $naturalSearchString = '';
380
        // This holds the result if the search is boolen (contains +/-/| operators)
381
        $booleanSearchString = '';
382
383
        $searchType = (string)$this->getSearchType();
384
385
        // Traverse searchwords and prefix them with corresponding operator
386
        foreach ($searchWordArray as $searchWordData) {
387
            // Making the query for a single search word based on the search-type
388
            $searchWord = $searchWordData['sword'];
389
            $wildcard = '';
390
            if (strstr($searchWord, ' ')) {
391
                $searchType = '20';
392
            }
393
            switch ($searchType) {
394
                case '1':
395
                case '2':
396
                case '3':
397
                    // First part of word
398
                    $wildcard = '*';
399
                    // Part-of-word search requires boolean mode!
400
                    $searchBoolean = true;
401
                    break;
402
                case '10':
403
                    $indexerObj = GeneralUtility::makeInstance(Indexer::class);
404
                    // Initialize the indexer-class
405
                    /** @var Indexer $indexerObj */
406
                    $searchWord = $indexerObj->metaphone($searchWord, $indexerObj->storeMetaphoneInfoAsWords);
407
                    unset($indexerObj);
408
                    $fulltextIndex = 'index_fulltext.metaphonedata';
409
                    break;
410
                case '20':
411
                    $searchBoolean = true;
412
                    // Remove existing quotes and fix misplaced quotes.
413
                    $searchWord = trim(str_replace('"', ' ', $searchWord));
414
                    break;
415
            }
416
            // Perform search for word:
417
            switch ($searchWordData['oper']) {
418
                case 'AND NOT':
419
                    $booleanSearchString .= ' -' . $searchWord . $wildcard;
420
                    $searchBoolean = true;
421
                    break;
422
                case 'OR':
423
                    $booleanSearchString .= ' ' . $searchWord . $wildcard;
424
                    $searchBoolean = true;
425
                    break;
426
                default:
427
                    $booleanSearchString .= ' +' . $searchWord . $wildcard;
428
                    $naturalSearchString .= ' ' . $searchWord;
429
            }
430
            $count++;
431
        }
432
        if ($searchType == '20') {
433
            $searchString = '"' . trim($naturalSearchString) . '"';
434
        } elseif ($searchBoolean) {
435
            $searchString = trim($booleanSearchString);
436
        } else {
437
            $searchString = trim($naturalSearchString);
438
        }
439
        return [
440
            'searchBoolean' => $searchBoolean,
441
            'searchString' => $searchString,
442
            'fulltextIndex' => $fulltextIndex
443
        ];
444
    }
445
446
    /**
447
     * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
448
     *
449
     * mysql fulltext specific helper method
450
     *
451
     * @param array $searchData Array with search string, boolean indicator, and fulltext index reference
452
     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
453
     * @return Statement
454
     */
455
    protected function execFinalQuery_fulltext($searchData, $freeIndexUid = -1)
456
    {
457
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_fulltext');
458
        $queryBuilder->getRestrictions()->removeAll();
459
        $queryBuilder->select('index_fulltext.*', 'ISEC.*', 'IP.*')
460
            ->from('index_fulltext')
461
            ->join(
462
                'index_fulltext',
463
                'index_phash',
464
                'IP',
465
                $queryBuilder->expr()->eq('index_fulltext.phash', $queryBuilder->quoteIdentifier('IP.phash'))
466
            )
467
            ->join(
468
                'IP',
469
                'index_section',
470
                'ISEC',
471
                $queryBuilder->expr()->eq('IP.phash', $queryBuilder->quoteIdentifier('ISEC.phash'))
472
            );
473
474
        // Calling hook for alternative creation of page ID list
475
        $searchRootPageIdList = $this->getSearchRootPageIdList();
476
        if ($hookObj = &$this->hookRequest('execFinalQuery_idList')) {
477
            $pageWhere = $hookObj->execFinalQuery_idList('');
478
            $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($pageWhere));
479
        } elseif ($this->joinPagesForQuery) {
480
            // Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
481
            $queryBuilder
482
                ->join(
483
                    'ISEC',
484
                    'pages',
485
                    'pages',
486
                    $queryBuilder->expr()->eq('ISEC.page_id', $queryBuilder->quoteIdentifier('pages.uid'))
487
                )
488
                ->andWhere(
489
                    $queryBuilder->expr()->eq(
490
                        'pages.no_search',
491
                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
492
                    )
493
                )
494
                ->andWhere(
495
                    $queryBuilder->expr()->lt(
496
                        'pages.doktype',
497
                        $queryBuilder->createNamedParameter(200, \PDO::PARAM_INT)
498
                    )
499
                );
500
            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
501
        } elseif ($searchRootPageIdList[0] >= 0) {
502
            // Collecting all pages IDs in which to search;
503
            // filtering out ALL pages that are not accessible due to restriction containers. Does NOT look for "no_search" field!
504
            $idList = [];
505
            foreach ($searchRootPageIdList as $rootId) {
506
                /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
507
                $cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
508
                $idList[] = $cObj->getTreeList(-1 * $rootId, 9999);
509
            }
510
            $idList = GeneralUtility::intExplode(',', implode(',', $idList));
511
            $queryBuilder->andWhere(
512
                $queryBuilder->expr()->in(
513
                    'ISEC.page_id',
514
                    $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
515
                )
516
            );
517
        }
518
519
        $searchBoolean = '';
520
        if ($searchData['searchBoolean']) {
521
            $searchBoolean = ' IN BOOLEAN MODE';
522
        }
523
        $queryBuilder->andWhere(
524
            'MATCH (' . $queryBuilder->quoteIdentifier($searchData['fulltextIndex']) . ')'
525
            . ' AGAINST (' . $queryBuilder->createNamedParameter($searchData['searchString'])
526
            . $searchBoolean
527
            . ')'
528
        );
529
530
        $queryBuilder->andWhere(
531
            QueryHelper::stripLogicalOperatorPrefix($this->mediaTypeWhere()),
532
            QueryHelper::stripLogicalOperatorPrefix($this->languageWhere()),
533
            QueryHelper::stripLogicalOperatorPrefix($this->freeIndexUidWhere($freeIndexUid)),
534
            QueryHelper::stripLogicalOperatorPrefix($this->sectionTableWhere())
535
        );
536
537
        $queryBuilder->groupBy(
538
            'IP.phash',
539
            'ISEC.phash',
540
            'ISEC.phash_t3',
541
            'ISEC.rl0',
542
            'ISEC.rl1',
543
            'ISEC.rl2',
544
            'ISEC.page_id',
545
            'ISEC.uniqid',
546
            'IP.phash_grouping',
547
            'IP.data_filename',
548
            'IP.data_page_id',
549
            'IP.data_page_reg1',
550
            'IP.data_page_type',
551
            'IP.data_page_mp',
552
            'IP.gr_list',
553
            'IP.item_type',
554
            'IP.item_title',
555
            'IP.item_description',
556
            'IP.item_mtime',
557
            'IP.tstamp',
558
            'IP.item_size',
559
            'IP.contentHash',
560
            'IP.crdate',
561
            'IP.parsetime',
562
            'IP.sys_language_uid',
563
            'IP.item_crdate',
564
            'IP.cHashParams',
565
            'IP.externalUrl',
566
            'IP.recordUid',
567
            'IP.freeIndexUid',
568
            'IP.freeIndexSetId'
569
        );
570
571
        return $queryBuilder->execute();
572
    }
573
574
    /***********************************
575
     *
576
     *	Helper functions on searching (SQL)
577
     *
578
     ***********************************/
579
    /**
580
     * Returns a COMPLETE list of phash-integers matching the search-result composed of the search-words in the $searchWords array.
581
     * The list of phash integers are unsorted and should be used for subsequent selection of index_phash records for display of the result.
582
     *
583
     * @param array $searchWords Search word array
584
     * @return string List of integers
585
     */
586
    protected function getPhashList($searchWords)
587
    {
588
        // Initialize variables:
589
        $c = 0;
590
        // This array accumulates the phash-values
591
        $totalHashList = [];
592
        $this->wSelClauses = [];
593
        // Traverse searchwords; for each, select all phash integers and merge/diff/intersect them with previous word (based on operator)
594
        foreach ($searchWords as $k => $v) {
595
            // Making the query for a single search word based on the search-type
596
            $sWord = $v['sword'];
597
            $theType = (string)$this->searchType;
598
            // If there are spaces in the search-word, make a full text search instead.
599
            if (strstr($sWord, ' ')) {
600
                $theType = 20;
601
            }
602
            $this->getTimeTracker()->push('SearchWord "' . $sWord . '" - $theType=' . $theType);
603
            // Perform search for word:
604
            switch ($theType) {
605
                case '1':
606
                    // Part of word
607
                    $res = $this->searchWord($sWord, Utility\LikeWildcard::BOTH);
608
                    break;
609
                case '2':
610
                    // First part of word
611
                    $res = $this->searchWord($sWord, Utility\LikeWildcard::RIGHT);
612
                    break;
613
                case '3':
614
                    // Last part of word
615
                    $res = $this->searchWord($sWord, Utility\LikeWildcard::LEFT);
616
                    break;
617
                case '10':
618
                    // Sounds like
619
                    /**
620
                    * Indexer object
621
                    *
622
                    * @var Indexer
623
                    */
624
                    $indexerObj = GeneralUtility::makeInstance(Indexer::class);
625
                    // Perform metaphone search
626
                    $storeMetaphoneInfoAsWords = !$this->isTableUsed('index_words');
627
                    $res = $this->searchMetaphone($indexerObj->metaphone($sWord, $storeMetaphoneInfoAsWords));
628
                    unset($indexerObj);
629
                    break;
630
                case '20':
631
                    // Sentence
632
                    $res = $this->searchSentence($sWord);
633
                    // If there is a fulltext search for a sentence there is
634
                    // a likeliness that sorting cannot be done by the rankings
635
                    // from the rel-table (because no relations will exist for the
636
                    // sentence in the word-table). So therefore mtime is used instead.
637
                    // It is not required, but otherwise some hits may be left out.
638
                    $this->sortOrder = 'mtime';
639
                    break;
640
                default:
641
                    // Distinct word
642
                    $res = $this->searchDistinct($sWord);
643
            }
644
            // If there was a query to do, then select all phash-integers which resulted from this.
645
            if ($res) {
646
                // Get phash list by searching for it:
647
                $phashList = [];
648
                while ($row = $res->fetch()) {
649
                    $phashList[] = $row['phash'];
650
                }
651
                // Here the phash list are merged with the existing result based on whether we are dealing with OR, NOT or AND operations.
652
                if ($c) {
653
                    switch ($v['oper']) {
654
                        case 'OR':
655
                            $totalHashList = array_unique(array_merge($phashList, $totalHashList));
656
                            break;
657
                        case 'AND NOT':
658
                            $totalHashList = array_diff($totalHashList, $phashList);
659
                            break;
660
                        default:
661
                            // AND...
662
                            $totalHashList = array_intersect($totalHashList, $phashList);
663
                    }
664
                } else {
665
                    // First search
666
                    $totalHashList = $phashList;
667
                }
668
            }
669
            $this->getTimeTracker()->pull();
670
            $c++;
671
        }
672
        return implode(',', $totalHashList);
673
    }
674
675
    /**
676
     * Returns a query which selects the search-word from the word/rel tables.
677
     *
678
     * @param string $wordSel WHERE clause selecting the word from phash
679
     * @param string $additionalWhereClause Additional AND clause in the end of the query.
680
     * @return Statement
681
     */
682
    protected function execPHashListQuery($wordSel, $additionalWhereClause = '')
683
    {
684
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_words');
685
        $queryBuilder->select('IR.phash')
686
            ->from('index_words', 'IW')
687
            ->from('index_rel', 'IR')
688
            ->from('index_section', 'ISEC')
689
            ->where(
690
                QueryHelper::stripLogicalOperatorPrefix($wordSel),
691
                $queryBuilder->expr()->eq('IW.wid', $queryBuilder->quoteIdentifier('IR.wid')),
692
                $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IR.phash')),
693
                QueryHelper::stripLogicalOperatorPrefix($this->sectionTableWhere()),
694
                QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
695
            )
696
            ->groupBy('IR.phash');
697
698
        return $queryBuilder->execute();
699
    }
700
701
    /**
702
     * Search for a word
703
     *
704
     * @param string $sWord the search word
705
     * @param int $wildcard Bit-field of Utility\LikeWildcard
706
     * @return Statement
707
     */
708
    protected function searchWord($sWord, $wildcard)
709
    {
710
        $likeWildcard = Utility\LikeWildcard::cast($wildcard);
711
        $wSel = $likeWildcard->getLikeQueryPart(
712
            'index_words',
713
            'IW.baseword',
714
            $sWord
715
        );
716
        $this->wSelClauses[] = $wSel;
717
        return $this->execPHashListQuery($wSel, ' AND is_stopword=0');
718
    }
719
720
    /**
721
     * Search for one distinct word
722
     *
723
     * @param string $sWord the search word
724
     * @return Statement
725
     */
726 View Code Duplication
    protected function searchDistinct($sWord)
727
    {
728
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
729
            ->getQueryBuilderForTable('index_words')
730
            ->expr();
731
        $wSel = $expressionBuilder->eq('IW.wid', $this->md5inthash($sWord));
732
        $this->wSelClauses[] = $wSel;
733
        return $this->execPHashListQuery($wSel, $expressionBuilder->eq('is_stopword', 0));
734
    }
735
736
    /**
737
     * Search for a sentence
738
     *
739
     * @param string $sWord the search word
740
     * @return Statement
741
     */
742
    protected function searchSentence($sWord)
743
    {
744
        $this->wSelClauses[] = '1=1';
745
        $likeWildcard = Utility\LikeWildcard::cast(Utility\LikeWildcard::BOTH);
746
        $likePart = $likeWildcard->getLikeQueryPart(
747
            'index_fulltext',
748
            'IFT.fulltextdata',
749
            $sWord
750
        );
751
752
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_section');
753
        return $queryBuilder->select('ISEC.phash')
754
            ->from('index_section', 'ISEC')
755
            ->from('index_fulltext', 'IFT')
756
            ->where(
757
                QueryHelper::stripLogicalOperatorPrefix($likePart),
758
                $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier(('IFT.phash'))),
759
                QueryHelper::stripLogicalOperatorPrefix($this->sectionTableWhere())
760
            )
761
            ->groupBy('ISEC.phash')
762
            ->execute();
763
    }
764
765
    /**
766
     * Search for a metaphone word
767
     *
768
     * @param string $sWord the search word
769
     * @return Statement
770
     */
771 View Code Duplication
    protected function searchMetaphone($sWord)
772
    {
773
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
774
            ->getQueryBuilderForTable('index_words')
775
            ->expr();
776
        $wSel = $expressionBuilder->eq('IW.metaphone', $expressionBuilder->literal($sWord));
777
        $this->wSelClauses[] = $wSel;
778
        return $this->execPHashListQuery($wSel, $expressionBuilder->eq('is_stopword', 0));
779
    }
780
781
    /**
782
     * Returns AND statement for selection of section in database. (rootlevel 0-2 + page_id)
783
     *
784
     * @return string AND clause for selection of section in database.
785
     */
786
    public function sectionTableWhere()
787
    {
788
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
789
            ->getQueryBuilderForTable('index_section')
790
            ->expr();
791
792
        $whereClause = $expressionBuilder->andX();
793
        $match = false;
794
        if (!($this->searchRootPageIdList < 0)) {
795
            $whereClause->add(
796
                $expressionBuilder->in('ISEC.rl0', GeneralUtility::intExplode(',', $this->searchRootPageIdList, true))
797
            );
798
        }
799
        if (substr($this->sections, 0, 4) === 'rl1_') {
800
            $whereClause->add(
801
                $expressionBuilder->in('ISEC.rl1', GeneralUtility::intExplode(',', substr($this->sections, 4)))
0 ignored issues
show
Bug introduced by
It seems like substr($this->sections, 4) can also be of type false; however, parameter $string of TYPO3\CMS\Core\Utility\G...alUtility::intExplode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

801
                $expressionBuilder->in('ISEC.rl1', GeneralUtility::intExplode(',', /** @scrutinizer ignore-type */ substr($this->sections, 4)))
Loading history...
802
            );
803
            $match = true;
804
        } elseif (substr($this->sections, 0, 4) === 'rl2_') {
805
            $whereClause->add(
806
                $expressionBuilder->in('ISEC.rl2', GeneralUtility::intExplode(',', substr($this->sections, 4)))
807
            );
808
            $match = true;
809
        } else {
810
            // Traversing user configured fields to see if any of those are used to limit search to a section:
811
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'] ?? [] as $fieldName => $rootLineLevel) {
812
                if (substr($this->sections, 0, strlen($fieldName) + 1) == $fieldName . '_') {
813
                    $whereClause->add(
814
                        $expressionBuilder->in(
815
                            'ISEC.' . $fieldName,
816
                            GeneralUtility::intExplode(',', substr($this->sections, strlen($fieldName) + 1))
817
                        )
818
                    );
819
                    $match = true;
820
                    break;
821
                }
822
            }
823
        }
824
        // If no match above, test the static types:
825
        if (!$match) {
826
            switch ((string)$this->sections) {
827
                case '-1':
828
                    $whereClause->add(
829
                        $expressionBuilder->eq('ISEC.page_id', (int)$this->getTypoScriptFrontendController()->id)
830
                    );
831
                    break;
832
                case '-2':
833
                    $whereClause->add($expressionBuilder->eq('ISEC.rl2', 0));
834
                    break;
835
                case '-3':
836
                    $whereClause->add($expressionBuilder->gt('ISEC.rl2', 0));
837
                    break;
838
            }
839
        }
840
841
        return $whereClause->count() ? ' AND ' . $whereClause : '';
842
    }
843
844
    /**
845
     * Returns AND statement for selection of media type
846
     *
847
     * @return string AND statement for selection of media type
848
     */
849
    public function mediaTypeWhere()
850
    {
851
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
852
            ->getQueryBuilderForTable('index_phash')
853
            ->expr();
854
        switch ($this->mediaType) {
855
            case '0':
856
                // '0' => 'only TYPO3 pages',
857
                $whereClause = $expressionBuilder->eq('IP.item_type', $expressionBuilder->literal('0'));
858
                break;
859
            case '-2':
860
                // All external documents
861
                $whereClause = $expressionBuilder->neq('IP.item_type', $expressionBuilder->literal('0'));
862
                break;
863
            case false:
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $this->mediaType of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
864
                // Intentional fall-through
865
            case '-1':
866
                // All content
867
                $whereClause = '';
868
                break;
869
            default:
870
                $whereClause = $expressionBuilder->eq('IP.item_type', $expressionBuilder->literal($this->mediaType));
871
        }
872
        return $whereClause ? ' AND ' . $whereClause : '';
873
    }
874
875
    /**
876
     * Returns AND statement for selection of language
877
     *
878
     * @return string AND statement for selection of language
879
     */
880
    public function languageWhere()
881
    {
882
        // -1 is the same as ALL language.
883
        if ($this->languageUid < 0) {
884
            return '';
885
        }
886
887
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
888
            ->getQueryBuilderForTable('index_phash')
889
            ->expr();
890
891
        return ' AND ' . $expressionBuilder->eq('IP.sys_language_uid', (int)$this->languageUid);
892
    }
893
894
    /**
895
     * Where-clause for free index-uid value.
896
     *
897
     * @param int $freeIndexUid Free Index UID value to limit search to.
898
     * @return string WHERE SQL clause part.
899
     */
900
    public function freeIndexUidWhere($freeIndexUid)
901
    {
902
        $freeIndexUid = (int)$freeIndexUid;
903
        if ($freeIndexUid < 0) {
904
            return '';
905
        }
906
        // First, look if the freeIndexUid is a meta configuration:
907
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
908
            ->getQueryBuilderForTable('index_config');
909
        $indexCfgRec = $queryBuilder->select('indexcfgs')
910
            ->from('index_config')
911
            ->where(
912
                $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(5, \PDO::PARAM_INT)),
913
                $queryBuilder->expr()->eq(
914
                    'uid',
915
                    $queryBuilder->createNamedParameter($freeIndexUid, \PDO::PARAM_INT)
916
                )
917
            )
918
            ->execute()
919
            ->fetch();
920
921
        if (is_array($indexCfgRec)) {
922
            $refs = GeneralUtility::trimExplode(',', $indexCfgRec['indexcfgs']);
923
            // Default value to protect against empty array.
924
            $list = [-99];
925
            foreach ($refs as $ref) {
926
                list($table, $uid) = GeneralUtility::revExplode('_', $ref, 2);
927
                $uid = (int)$uid;
928
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
929
                    ->getQueryBuilderForTable('index_config');
930
                $queryBuilder->select('uid')
931
                    ->from('index_config');
932
                switch ($table) {
933 View Code Duplication
                    case 'index_config':
934
                        $idxRec = $queryBuilder
935
                            ->where(
936
                                $queryBuilder->expr()->eq(
937
                                    'uid',
938
                                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
939
                                )
940
                            )
941
                            ->execute()
942
                            ->fetch();
943
                        if ($idxRec) {
944
                            $list[] = $uid;
945
                        }
946
                        break;
947 View Code Duplication
                    case 'pages':
948
                        $indexCfgRecordsFromPid = $queryBuilder
949
                            ->where(
950
                                $queryBuilder->expr()->eq(
951
                                    'pid',
952
                                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
953
                                )
954
                            )
955
                            ->execute();
956
                        while ($idxRec = $indexCfgRecordsFromPid->fetch()) {
957
                            $list[] = $idxRec['uid'];
958
                        }
959
                        break;
960
                }
961
            }
962
            $list = array_unique($list);
963
        } else {
964
            $list = [$freeIndexUid];
965
        }
966
967
        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
968
            ->getQueryBuilderForTable('index_phash')
969
            ->expr();
970
        return ' AND ' . $expressionBuilder->in('IP.freeIndexUid', array_map('intval', $list));
971
    }
972
973
    /**
974
     * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
975
     *
976
     * @param string $list List of phash integers which match the search.
977
     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
978
     * @return Statement
979
     */
980
    protected function execFinalQuery($list, $freeIndexUid = -1)
981
    {
982
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_words');
983
        $queryBuilder->select('ISEC.*', 'IP.*')
984
            ->from('index_phash', 'IP')
985
            ->from('index_section', 'ISEC')
986
            ->where(
987
                $queryBuilder->expr()->in(
988
                    'IP.phash',
989
                    $queryBuilder->createNamedParameter(
990
                        GeneralUtility::intExplode(',', $list, true),
991
                        Connection::PARAM_INT_ARRAY
992
                    )
993
                ),
994
                QueryHelper::stripLogicalOperatorPrefix($this->mediaTypeWhere()),
995
                QueryHelper::stripLogicalOperatorPrefix($this->languageWhere()),
996
                QueryHelper::stripLogicalOperatorPrefix($this->freeIndexUidWhere($freeIndexUid)),
997
                $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IP.phash'))
998
            )
999
            ->groupBy(
1000
                'IP.phash',
1001
                'ISEC.phash',
1002
                'ISEC.phash_t3',
1003
                'ISEC.rl0',
1004
                'ISEC.rl1',
1005
                'ISEC.rl2',
1006
                'ISEC.page_id',
1007
                'ISEC.uniqid',
1008
                'IP.phash_grouping',
1009
                'IP.data_filename',
1010
                'IP.data_page_id',
1011
                'IP.data_page_reg1',
1012
                'IP.data_page_type',
1013
                'IP.data_page_mp',
1014
                'IP.gr_list',
1015
                'IP.item_type',
1016
                'IP.item_title',
1017
                'IP.item_description',
1018
                'IP.item_mtime',
1019
                'IP.tstamp',
1020
                'IP.item_size',
1021
                'IP.contentHash',
1022
                'IP.crdate',
1023
                'IP.parsetime',
1024
                'IP.sys_language_uid',
1025
                'IP.item_crdate',
1026
                'IP.cHashParams',
1027
                'IP.externalUrl',
1028
                'IP.recordUid',
1029
                'IP.freeIndexUid',
1030
                'IP.freeIndexSetId'
1031
            );
1032
1033
        // Setting up methods of filtering results
1034
        // based on page types, access, etc.
1035
        if ($hookObj = $this->hookRequest('execFinalQuery_idList')) {
1036
            // Calling hook for alternative creation of page ID list
1037
            $hookWhere = QueryHelper::stripLogicalOperatorPrefix($hookObj->execFinalQuery_idList($list));
1038
            if (!empty($hookWhere)) {
1039
                $queryBuilder->andWhere($hookWhere);
1040
            }
1041
        } elseif ($this->joinPagesForQuery) {
1042
            // Alternative to getting all page ids by ->getTreeList() where
1043
            // "excludeSubpages" is NOT respected.
1044
            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1045
            $queryBuilder->from('pages');
1046
            $queryBuilder->andWhere(
1047
                $queryBuilder->expr()->eq('pages.uid', $queryBuilder->quoteIdentifier('ISEC.page_id')),
1048
                $queryBuilder->expr()->eq(
1049
                    'pages.no_search',
1050
                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1051
                ),
1052
                $queryBuilder->expr()->lt(
1053
                    'pages.doktype',
1054
                    $queryBuilder->createNamedParameter(200, \PDO::PARAM_INT)
1055
                )
1056
            );
1057
        } elseif ($this->searchRootPageIdList >= 0) {
1058
            // Collecting all pages IDs in which to search;
1059
            // filtering out ALL pages that are not accessible due to restriction containers.
1060
            // Does NOT look for "no_search" field!
1061
            $siteIdNumbers = GeneralUtility::intExplode(',', $this->searchRootPageIdList);
1062
            $pageIdList = [];
1063
            foreach ($siteIdNumbers as $rootId) {
1064
                $pageIdList[] = $this->getTypoScriptFrontendController()->cObj->getTreeList(-1 * $rootId, 9999);
1065
            }
1066
            $queryBuilder->andWhere(
1067
                $queryBuilder->expr()->in(
1068
                    'ISEC.page_id',
1069
                    $queryBuilder->createNamedParameter(
1070
                        array_unique(GeneralUtility::intExplode(',', implode(',', $pageIdList), true)),
1071
                        Connection::PARAM_INT_ARRAY
1072
                    )
1073
                )
1074
            );
1075
        }
1076
        // otherwise select all / disable everything
1077
        // If any of the ranking sortings are selected, we must make a
1078
        // join with the word/rel-table again, because we need to
1079
        // calculate ranking based on all search-words found.
1080
        if (substr($this->sortOrder, 0, 5) === 'rank_') {
1081
            $queryBuilder
1082
                ->from('index_words', 'IW')
1083
                ->from('index_rel', 'IR')
1084
                ->andWhere(
1085
                    $queryBuilder->expr()->eq('IW.wid', $queryBuilder->quoteIdentifier('IR.wid')),
1086
                    $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IR.phash'))
1087
                );
1088
            switch ($this->sortOrder) {
1089
                case 'rank_flag':
1090
                    // This gives priority to word-position (max-value) so that words in title, keywords, description counts more than in content.
1091
                    // The ordering is refined with the frequency sum as well.
1092
                    $queryBuilder
1093
                        ->addSelectLiteral(
1094
                            $queryBuilder->expr()->max('IR.flags', 'order_val1'),
1095
                            $queryBuilder->expr()->sum('IR.freq', 'order_val2')
1096
                        )
1097
                        ->orderBy('order_val1', $this->getDescendingSortOrderFlag())
1098
                        ->addOrderBy('order_val2', $this->getDescendingSortOrderFlag());
1099
                    break;
1100
                case 'rank_first':
1101
                    // Results in average position of search words on page.
1102
                    // Must be inversely sorted (low numbers are closer to top)
1103
                    $queryBuilder
1104
                        ->addSelectLiteral($queryBuilder->expr()->avg('IR.first', 'order_val'))
1105
                        ->orderBy('order_val', $this->getDescendingSortOrderFlag(true));
1106
                    break;
1107
                case 'rank_count':
1108
                    // Number of words found
1109
                    $queryBuilder
1110
                        ->addSelectLiteral($queryBuilder->expr()->sum('IR.count', 'order_val'))
1111
                        ->orderBy('order_val', $this->getDescendingSortOrderFlag());
1112
                    break;
1113
                default:
1114
                    // Frequency sum. I'm not sure if this is the best way to do
1115
                    // it (make a sum...). Or should it be the average?
1116
                    $queryBuilder
1117
                        ->addSelectLiteral($queryBuilder->expr()->sum('IR.freq', 'order_val'))
1118
                        ->orderBy('order_val', $this->getDescendingSortOrderFlag());
1119
            }
1120
1121
            if (!empty($this->wSelClauses)) {
1122
                // So, words are combined in an OR statement
1123
                // (no "sentence search" should be done here - may deselect results)
1124
                $wordSel = $queryBuilder->expr()->orX();
1125
                foreach ($this->wSelClauses as $wSelClause) {
1126
                    $wordSel->add(QueryHelper::stripLogicalOperatorPrefix($wSelClause));
1127
                }
1128
                $queryBuilder->andWhere($wordSel);
1129
            }
1130
        } else {
1131
            // Otherwise, if sorting are done with the pages table or other fields,
1132
            // there is no need for joining with the rel/word tables:
1133
            switch ((string)$this->sortOrder) {
1134
                case 'title':
1135
                    $queryBuilder->orderBy('IP.item_title', $this->getDescendingSortOrderFlag());
1136
                    break;
1137
                case 'crdate':
1138
                    $queryBuilder->orderBy('IP.item_crdate', $this->getDescendingSortOrderFlag());
1139
                    break;
1140
                case 'mtime':
1141
                    $queryBuilder->orderBy('IP.item_mtime', $this->getDescendingSortOrderFlag());
1142
                    break;
1143
            }
1144
        }
1145
1146
        return $queryBuilder->execute();
1147
    }
1148
1149
    /**
1150
     * Checking if the resume can be shown for the search result
1151
     * (depending on whether the rights are OK)
1152
     * ? Should it also check for gr_list "0,-1"?
1153
     *
1154
     * @param array $row Result row array.
1155
     * @return bool Returns TRUE if resume can safely be shown
1156
     */
1157
    protected function checkResume($row)
1158
    {
1159
        // If the record is indexed by an indexing configuration, just show it.
1160
        // At least this is needed for external URLs and files.
1161
        // For records we might need to extend this - for instance block display if record is access restricted.
1162
        if ($row['freeIndexUid']) {
1163
            return true;
1164
        }
1165
        // Evaluate regularly indexed pages based on item_type:
1166
        // External media:
1167
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('index_grlist');
1168 View Code Duplication
        if ($row['item_type']) {
1169
            // For external media we will check the access of the parent page on which the media was linked from.
1170
            // "phash_t3" is the phash of the parent TYPO3 page row which initiated the indexing of the documents
1171
            // in this section. So, selecting for the grlist records belonging to the parent phash-row where the
1172
            // current users gr_list exists will help us to know. If this is NOT found, there is still a theoretical
1173
            // possibility that another user accessible page would display a link, so maybe the resume of such a
1174
            // document here may be unjustified hidden. But better safe than sorry.
1175
            if (!$this->isTableUsed('index_grlist')) {
1176
                return false;
1177
            }
1178
1179
            return (bool)$connection->count(
1180
                'phash',
1181
                'index_grlist',
1182
                [
1183
                    'phash' => (int)$row['phash_t3'],
1184
                    'gr_list' => $this->frontendUserGroupList
1185
                ]
1186
            );
1187
        }
1188
        // Ordinary TYPO3 pages:
1189 View Code Duplication
        if ((string)$row['gr_list'] !== (string)$this->frontendUserGroupList) {
1190
            // Selecting for the grlist records belonging to the phash-row where the current users gr_list exists.
1191
            // If it is found it is proof that this user has direct access to the phash-rows content although
1192
            // he did not himself initiate the indexing...
1193
            if (!$this->isTableUsed('index_grlist')) {
1194
                return false;
1195
            }
1196
1197
            return (bool)$connection->count(
1198
                    'phash',
1199
                    'index_grlist',
1200
                    [
1201
                        'phash' => (int)$row['phash'],
1202
                        'gr_list' => $this->frontendUserGroupList
1203
                    ]
1204
                );
1205
        }
1206
        return true;
1207
    }
1208
1209
    /**
1210
     * Returns "DESC" or "" depending on the settings of the incoming
1211
     * highest/lowest result order (piVars['desc'])
1212
     *
1213
     * @param bool $inverse If TRUE, inverse the order which is defined by piVars['desc']
1214
     * @return string " DESC" or formerly known as tx_indexedsearch_pi->isDescending
1215
     */
1216
    protected function getDescendingSortOrderFlag($inverse = false)
1217
    {
1218
        $desc = $this->descendingSortOrderFlag;
1219
        if ($inverse) {
1220
            $desc = !$desc;
1221
        }
1222
        return !$desc ? ' DESC' : '';
1223
    }
1224
1225
    /**
1226
     * Returns if an item type is a multipage item type
1227
     *
1228
     * @param string $itemType Item type
1229
     * @return bool TRUE if multipage capable
1230
     */
1231
    protected function multiplePagesType($itemType)
1232
    {
1233
        /** @var \TYPO3\CMS\IndexedSearch\FileContentParser $fileContentParser */
1234
        $fileContentParser = $this->externalParsers[$itemType];
1235
        return is_object($fileContentParser) && $fileContentParser->isMultiplePageExtension($itemType);
1236
    }
1237
1238
    /**
1239
     * md5 integer hash
1240
     * Using 7 instead of 8 just because that makes the integers lower than
1241
     * 32 bit (28 bit) and so they do not interfere with UNSIGNED integers
1242
     * or PHP-versions which has varying output from the hexdec function.
1243
     *
1244
     * @param string $str String to hash
1245
     * @return int Integer interpretation of the md5 hash of input string.
1246
     */
1247
    protected function md5inthash($str)
1248
    {
1249
        return Utility\IndexedSearchUtility::md5inthash($str);
1250
    }
1251
1252
    /**
1253
     * Check if the tables provided are configured for usage.
1254
     * This becomes necessary for extensions that provide additional database
1255
     * functionality like indexed_search_mysql.
1256
     *
1257
     * @param string $table_list Comma-separated list of tables
1258
     * @return bool TRUE if given tables are enabled
1259
     */
1260
    protected function isTableUsed($table_list)
1261
    {
1262
        return Utility\IndexedSearchUtility::isTableUsed($table_list);
1263
    }
1264
1265
    /**
1266
     * Returns an object reference to the hook object if any
1267
     *
1268
     * @param string $functionName Name of the function you want to call / hook key
1269
     * @return object|null Hook object, if any. Otherwise NULL.
1270
     */
1271 View Code Duplication
    public function hookRequest($functionName)
1272
    {
1273
        // Hook: menuConfig_preProcessModMenu
1274
        if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
1275
            $hookObj = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
1276
            if (method_exists($hookObj, $functionName)) {
1277
                $hookObj->pObj = $this;
1278
                return $hookObj;
1279
            }
1280
        }
1281
        return null;
1282
    }
1283
1284
    /**
1285
     * Search type
1286
     * e.g. sentence (20), any part of the word (1)
1287
     *
1288
     * @return int
1289
     */
1290
    public function getSearchType()
1291
    {
1292
        return (int)$this->searchType;
1293
    }
1294
1295
    /**
1296
     * A list of integer which should be root-pages to search from
1297
     *
1298
     * @return int[]
1299
     */
1300
    public function getSearchRootPageIdList()
1301
    {
1302
        return GeneralUtility::intExplode(',', $this->searchRootPageIdList);
1303
    }
1304
1305
    /**
1306
     * Getter for joinPagesForQuery flag
1307
     * enabled through TypoScript 'settings.skipExtendToSubpagesChecking'
1308
     *
1309
     * @return bool
1310
     */
1311
    public function getJoinPagesForQuery()
1312
    {
1313
        return $this->joinPagesForQuery;
1314
    }
1315
1316
    /**
1317
     * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
1318
     */
1319
    protected function getTypoScriptFrontendController()
1320
    {
1321
        return $GLOBALS['TSFE'];
1322
    }
1323
1324
    /**
1325
     * @return TimeTracker
1326
     */
1327
    protected function getTimeTracker()
1328
    {
1329
        return GeneralUtility::makeInstance(TimeTracker::class);
1330
    }
1331
}
1332