1<?php
2namespace 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
17use Doctrine\DBAL\Driver\Statement;
18use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
19use TYPO3\CMS\Core\Context\Context;
20use TYPO3\CMS\Core\Database\Connection;
21use TYPO3\CMS\Core\Database\ConnectionPool;
22use TYPO3\CMS\Core\Database\Query\QueryHelper;
23use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
24use TYPO3\CMS\Core\TimeTracker\TimeTracker;
25use TYPO3\CMS\Core\Utility\GeneralUtility;
26use TYPO3\CMS\Core\Utility\MathUtility;
27use TYPO3\CMS\IndexedSearch\Indexer;
28use TYPO3\CMS\IndexedSearch\Utility;
29
30/**
31 * Index search abstraction to search through the index
32 * @internal This class is a specific repository implementation and is not considered part of the Public TYPO3 API.
33 */
34class IndexSearchRepository
35{
36    /**
37     * Indexer object
38     *
39     * @var Indexer
40     */
41    protected $indexerObj;
42
43    /**
44     * External Parsers
45     *
46     * @var array
47     */
48    protected $externalParsers = [];
49
50    /**
51     * Frontend User Group List
52     *
53     * @var string
54     */
55    protected $frontendUserGroupList = '';
56
57    /**
58     * Sections
59     * formally known as $this->piVars['sections']
60     *
61     * @var string
62     */
63    protected $sections;
64
65    /**
66     * Search type
67     * formally known as $this->piVars['type']
68     *
69     * @var string
70     */
71    protected $searchType;
72
73    /**
74     * Language uid
75     * formally known as $this->piVars['lang']
76     *
77     * @var int
78     */
79    protected $languageUid;
80
81    /**
82     * Media type
83     * formally known as $this->piVars['media']
84     *
85     * @var int
86     */
87    protected $mediaType;
88
89    /**
90     * Sort order
91     * formally known as $this->piVars['sort_order']
92     *
93     * @var string
94     */
95    protected $sortOrder;
96
97    /**
98     * Descending sort order flag
99     * formally known as $this->piVars['desc']
100     *
101     * @var bool
102     */
103    protected $descendingSortOrderFlag;
104
105    /**
106     * Result page pointer
107     * formally known as $this->piVars['pointer']
108     *
109     * @var int
110     */
111    protected $resultpagePointer = 0;
112
113    /**
114     * Number of results
115     * formally known as $this->piVars['result']
116     *
117     * @var int
118     */
119    protected $numberOfResults = 10;
120
121    /**
122     * list of all root pages that will be used
123     * If this value is set to less than zero (eg. -1) searching will happen
124     * in ALL of the page tree with no regard to branches at all.
125     *
126     * @var string
127     */
128    protected $searchRootPageIdList;
129
130    /**
131     * formally known as $conf['search.']['searchSkipExtendToSubpagesChecking']
132     * enabled through settings.searchSkipExtendToSubpagesChecking
133     *
134     * @var bool
135     */
136    protected $joinPagesForQuery = false;
137
138    /**
139     * Select clauses for individual words, will be filled during the search
140     *
141     * @var array
142     */
143    protected $wSelClauses = [];
144
145    /**
146     * Flag for exact search count
147     * formally known as $conf['search.']['exactCount']
148     *
149     * Continue counting and checking of results even if we are sure
150     * they are not displayed in this request. This will slow down your
151     * page rendering, but it allows precise search result counters.
152     * enabled through settings.exactCount
153     *
154     * @var bool
155     */
156    protected $useExactCount = false;
157
158    /**
159     * Display forbidden records
160     * formally known as $this->conf['show.']['forbiddenRecords']
161     *
162     * enabled through settings.displayForbiddenRecords
163     *
164     * @var bool
165     */
166    protected $displayForbiddenRecords = false;
167
168    /**
169     * initialize all options that are necessary for the search
170     *
171     * @param array $settings the extbase plugin settings
172     * @param array $searchData the search data
173     * @param array $externalParsers
174     * @param string $searchRootPageIdList
175     */
176    public function initialize($settings, $searchData, $externalParsers, $searchRootPageIdList)
177    {
178        // Initialize the indexer-class - just to use a few function (for making hashes)
179        $this->indexerObj = GeneralUtility::makeInstance(Indexer::class);
180        $this->externalParsers = $externalParsers;
181        $this->searchRootPageIdList = $searchRootPageIdList;
182        $this->frontendUserGroupList = implode(',', GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'groupIds', [0, -1]));
183        // Should we use joinPagesForQuery instead of long lists of uids?
184        if ($settings['searchSkipExtendToSubpagesChecking']) {
185            $this->joinPagesForQuery = 1;
186        }
187        if ($settings['exactCount']) {
188            $this->useExactCount = true;
189        }
190        if ($settings['displayForbiddenRecords']) {
191            $this->displayForbiddenRecords = true;
192        }
193        $this->sections = $searchData['sections'];
194        $this->searchType = $searchData['searchType'];
195        $this->languageUid = $searchData['languageUid'];
196        $this->mediaType = $searchData['mediaType'] ?? false;
197        $this->sortOrder = $searchData['sortOrder'];
198        $this->descendingSortOrderFlag = $searchData['desc'];
199        $this->resultpagePointer = $searchData['pointer'];
200        if (isset($searchData['numberOfResults']) && is_numeric($searchData['numberOfResults'])) {
201            $this->numberOfResults = (int)$searchData['numberOfResults'];
202        }
203    }
204
205    /**
206     * Get search result rows / data from database. Returned as data in array.
207     *
208     * @param array $searchWords Search word array
209     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
210     * @return bool|array FALSE if no result, otherwise an array with keys for first row, result rows and total number of results found.
211     */
212    public function doSearch($searchWords, $freeIndexUid = -1)
213    {
214        $useMysqlFulltext = (bool)GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('indexed_search', 'useMysqlFulltext');
215        // Getting SQL result pointer:
216        $this->getTimeTracker()->push('Searching result');
217        if ($hookObj = &$this->hookRequest('getResultRows_SQLpointer')) {
218            $result = $hookObj->getResultRows_SQLpointer($searchWords, $freeIndexUid);
219        } elseif ($useMysqlFulltext) {
220            $result = $this->getResultRows_SQLpointerMysqlFulltext($searchWords, $freeIndexUid);
221        } else {
222            $result = $this->getResultRows_SQLpointer($searchWords, $freeIndexUid);
223        }
224        $this->getTimeTracker()->pull();
225        // Organize and process result:
226        if ($result) {
227            // Total search-result count
228            $count = $result->rowCount();
229            // The pointer is set to the result page that is currently being viewed
230            $pointer = MathUtility::forceIntegerInRange($this->resultpagePointer, 0, floor($count / $this->numberOfResults));
231            // Initialize result accumulation variables:
232            $c = 0;
233            // Result pointer: Counts up the position in the current search-result
234            $grouping_phashes = [];
235            // Used to filter out duplicates.
236            $grouping_chashes = [];
237            // Used to filter out duplicates BASED ON cHash.
238            $firstRow = [];
239            // Will hold the first row in result - used to calculate relative hit-ratings.
240            $resultRows = [];
241            // Will hold the results rows for display.
242            // Now, traverse result and put the rows to be displayed into an array
243            // Each row should contain the fields from 'ISEC.*, IP.*' combined
244            // + artificial fields "show_resume" (bool) and "result_number" (counter)
245            while ($row = $result->fetch()) {
246                // Set first row
247                if (!$c) {
248                    $firstRow = $row;
249                }
250                // Tells whether we can link directly to a document
251                // or not (depends on possible right problems)
252                $row['show_resume'] = $this->checkResume($row);
253                $phashGr = !in_array($row['phash_grouping'], $grouping_phashes);
254                $chashGr = !in_array($row['contentHash'] . '.' . $row['data_page_id'], $grouping_chashes);
255                if ($phashGr && $chashGr) {
256                    // Only if the resume may be shown are we going to filter out duplicates...
257                    if ($row['show_resume'] || $this->displayForbiddenRecords) {
258                        // Only on documents which are not multiple pages documents
259                        if (!$this->multiplePagesType($row['item_type'])) {
260                            $grouping_phashes[] = $row['phash_grouping'];
261                        }
262                        $grouping_chashes[] = $row['contentHash'] . '.' . $row['data_page_id'];
263                        // Increase the result pointer
264                        $c++;
265                        // All rows for display is put into resultRows[]
266                        if ($c > $pointer * $this->numberOfResults && $c <= $pointer * $this->numberOfResults + $this->numberOfResults) {
267                            $row['result_number'] = $c;
268                            $resultRows[] = $row;
269                            // This may lead to a problem: If the result check is not stopped here, the search will take longer.
270                            // However the result counter will not filter out grouped cHashes/pHashes that were not processed yet.
271                            // You can change this behavior using the "settings.exactCount" property (see above).
272                            if (!$this->useExactCount && $c + 1 > ($pointer + 1) * $this->numberOfResults) {
273                                break;
274                            }
275                        }
276                    } else {
277                        // Skip this row if the user cannot
278                        // view it (missing permission)
279                        $count--;
280                    }
281                } else {
282                    // For each time a phash_grouping document is found
283                    // (which is thus not displayed) the search-result count is reduced,
284                    // so that it matches the number of rows displayed.
285                    $count--;
286                }
287            }
288
289            $result->closeCursor();
290
291            return [
292                'resultRows' => $resultRows,
293                'firstRow' => $firstRow,
294                'count' => $count
295            ];
296        }
297        // No results found
298        return false;
299    }
300
301    /**
302     * Gets a SQL result pointer to traverse for the search records.
303     *
304     * @param array $searchWords Search words
305     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
306     * @return Statement
307     */
308    protected function getResultRows_SQLpointer($searchWords, $freeIndexUid = -1)
309    {
310        // This SEARCHES for the searchwords in $searchWords AND returns a
311        // COMPLETE list of phash-integers of the matches.
312        $list = $this->getPhashList($searchWords);
313        // Perform SQL Search / collection of result rows array:
314        if ($list) {
315            // Do the search:
316            $this->getTimeTracker()->push('execFinalQuery');
317            $res = $this->execFinalQuery($list, $freeIndexUid);
318            $this->getTimeTracker()->pull();
319            return $res;
320        }
321        return false;
322    }
323
324    /**
325     * Gets a SQL result pointer to traverse for the search records.
326     *
327     * mysql fulltext specific version triggered by ext_conf_template setting 'useMysqlFulltext'
328     *
329     * @param array $searchWordsArray Search words
330     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
331     * @return bool|\mysqli_result|object MySQLi result object / DBAL object
332     */
333    protected function getResultRows_SQLpointerMysqlFulltext($searchWordsArray, $freeIndexUid = -1)
334    {
335        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('index_fulltext');
336        if (strpos($connection->getServerVersion(), 'MySQL') !== 0) {
337            throw new \RuntimeException(
338                'Extension indexed_search is configured to use mysql fulltext, but table \'index_fulltext\''
339                . ' is running on a different DBMS.',
340                1472585525
341            );
342        }
343        // Build the search string, detect which fulltext index to use, and decide whether boolean search is needed or not
344        $searchData = $this->getSearchString($searchWordsArray);
345        // Perform SQL Search / collection of result rows array:
346        $resource = false;
347        if ($searchData) {
348            /** @var TimeTracker $timeTracker */
349            $timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
350            // Do the search:
351            $timeTracker->push('execFinalQuery');
352            $resource = $this->execFinalQuery_fulltext($searchData, $freeIndexUid);
353            $timeTracker->pull();
354        }
355        return $resource;
356    }
357
358    /**
359     * Returns a search string for use with MySQL FULLTEXT query
360     *
361     * mysql fulltext specific helper method
362     *
363     * @param array $searchWordArray Search word array
364     * @return array Search string
365     */
366    protected function getSearchString($searchWordArray)
367    {
368        // Initialize variables:
369        $count = 0;
370        // Change this to TRUE to force BOOLEAN SEARCH MODE (useful if fulltext index is still empty)
371        $searchBoolean = false;
372        $fulltextIndex = 'index_fulltext.fulltextdata';
373        // This holds the result if the search is natural (doesn't contain any boolean operators)
374        $naturalSearchString = '';
375        // This holds the result if the search is boolen (contains +/-/| operators)
376        $booleanSearchString = '';
377
378        $searchType = (string)$this->getSearchType();
379
380        // Traverse searchwords and prefix them with corresponding operator
381        foreach ($searchWordArray as $searchWordData) {
382            // Making the query for a single search word based on the search-type
383            $searchWord = $searchWordData['sword'];
384            $wildcard = '';
385            if (strstr($searchWord, ' ')) {
386                $searchType = '20';
387            }
388            switch ($searchType) {
389                case '1':
390                case '2':
391                case '3':
392                    // First part of word
393                    $wildcard = '*';
394                    // Part-of-word search requires boolean mode!
395                    $searchBoolean = true;
396                    break;
397                case '10':
398                    $indexerObj = GeneralUtility::makeInstance(Indexer::class);
399                    // Initialize the indexer-class
400                    /** @var Indexer $indexerObj */
401                    $searchWord = $indexerObj->metaphone($searchWord, $indexerObj->storeMetaphoneInfoAsWords);
402                    unset($indexerObj);
403                    $fulltextIndex = 'index_fulltext.metaphonedata';
404                    break;
405                case '20':
406                    $searchBoolean = true;
407                    // Remove existing quotes and fix misplaced quotes.
408                    $searchWord = trim(str_replace('"', ' ', $searchWord));
409                    break;
410            }
411            // Perform search for word:
412            switch ($searchWordData['oper']) {
413                case 'AND NOT':
414                    $booleanSearchString .= ' -' . $searchWord . $wildcard;
415                    $searchBoolean = true;
416                    break;
417                case 'OR':
418                    $booleanSearchString .= ' ' . $searchWord . $wildcard;
419                    $searchBoolean = true;
420                    break;
421                default:
422                    $booleanSearchString .= ' +' . $searchWord . $wildcard;
423                    $naturalSearchString .= ' ' . $searchWord;
424            }
425            $count++;
426        }
427        if ($searchType == '20') {
428            $searchString = '"' . trim($naturalSearchString) . '"';
429        } elseif ($searchBoolean) {
430            $searchString = trim($booleanSearchString);
431        } else {
432            $searchString = trim($naturalSearchString);
433        }
434        return [
435            'searchBoolean' => $searchBoolean,
436            'searchString' => $searchString,
437            'fulltextIndex' => $fulltextIndex
438        ];
439    }
440
441    /**
442     * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
443     *
444     * mysql fulltext specific helper method
445     *
446     * @param array $searchData Array with search string, boolean indicator, and fulltext index reference
447     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
448     * @return Statement
449     */
450    protected function execFinalQuery_fulltext($searchData, $freeIndexUid = -1)
451    {
452        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_fulltext');
453        $queryBuilder->getRestrictions()->removeAll();
454        $queryBuilder->select('index_fulltext.*', 'ISEC.*', 'IP.*')
455            ->from('index_fulltext')
456            ->join(
457                'index_fulltext',
458                'index_phash',
459                'IP',
460                $queryBuilder->expr()->eq('index_fulltext.phash', $queryBuilder->quoteIdentifier('IP.phash'))
461            )
462            ->join(
463                'IP',
464                'index_section',
465                'ISEC',
466                $queryBuilder->expr()->eq('IP.phash', $queryBuilder->quoteIdentifier('ISEC.phash'))
467            );
468
469        // Calling hook for alternative creation of page ID list
470        $searchRootPageIdList = $this->getSearchRootPageIdList();
471        if ($hookObj = &$this->hookRequest('execFinalQuery_idList')) {
472            $pageWhere = $hookObj->execFinalQuery_idList('');
473            $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($pageWhere));
474        } elseif ($this->joinPagesForQuery) {
475            // Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
476            $queryBuilder
477                ->join(
478                    'ISEC',
479                    'pages',
480                    'pages',
481                    $queryBuilder->expr()->eq('ISEC.page_id', $queryBuilder->quoteIdentifier('pages.uid'))
482                )
483                ->andWhere(
484                    $queryBuilder->expr()->eq(
485                        'pages.no_search',
486                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
487                    )
488                )
489                ->andWhere(
490                    $queryBuilder->expr()->lt(
491                        'pages.doktype',
492                        $queryBuilder->createNamedParameter(200, \PDO::PARAM_INT)
493                    )
494                );
495            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
496        } elseif ($searchRootPageIdList[0] >= 0) {
497            // Collecting all pages IDs in which to search;
498            // filtering out ALL pages that are not accessible due to restriction containers. Does NOT look for "no_search" field!
499            $idList = [];
500            foreach ($searchRootPageIdList as $rootId) {
501                /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
502                $cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
503                $idList[] = $cObj->getTreeList(-1 * $rootId, 9999);
504            }
505            $idList = GeneralUtility::intExplode(',', implode(',', $idList));
506            $queryBuilder->andWhere(
507                $queryBuilder->expr()->in(
508                    'ISEC.page_id',
509                    $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
510                )
511            );
512        }
513
514        $searchBoolean = '';
515        if ($searchData['searchBoolean']) {
516            $searchBoolean = ' IN BOOLEAN MODE';
517        }
518        $queryBuilder->andWhere(
519            'MATCH (' . $queryBuilder->quoteIdentifier($searchData['fulltextIndex']) . ')'
520            . ' AGAINST (' . $queryBuilder->createNamedParameter($searchData['searchString'])
521            . $searchBoolean
522            . ')'
523        );
524
525        $queryBuilder->andWhere(
526            QueryHelper::stripLogicalOperatorPrefix($this->mediaTypeWhere()),
527            QueryHelper::stripLogicalOperatorPrefix($this->languageWhere()),
528            QueryHelper::stripLogicalOperatorPrefix($this->freeIndexUidWhere($freeIndexUid)),
529            QueryHelper::stripLogicalOperatorPrefix($this->sectionTableWhere())
530        );
531
532        $queryBuilder->groupBy(
533            'IP.phash',
534            'ISEC.phash',
535            'ISEC.phash_t3',
536            'ISEC.rl0',
537            'ISEC.rl1',
538            'ISEC.rl2',
539            'ISEC.page_id',
540            'ISEC.uniqid',
541            'IP.phash_grouping',
542            'IP.data_filename',
543            'IP.data_page_id',
544            // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Remove along with database field data_page_reg1
545            'IP.data_page_reg1',
546            'IP.data_page_type',
547            'IP.data_page_mp',
548            'IP.gr_list',
549            'IP.item_type',
550            'IP.item_title',
551            'IP.item_description',
552            'IP.item_mtime',
553            'IP.tstamp',
554            'IP.item_size',
555            'IP.contentHash',
556            'IP.crdate',
557            'IP.parsetime',
558            'IP.sys_language_uid',
559            'IP.item_crdate',
560            'IP.cHashParams',
561            'IP.externalUrl',
562            'IP.recordUid',
563            'IP.freeIndexUid',
564            'IP.freeIndexSetId'
565        );
566
567        return $queryBuilder->execute();
568    }
569
570    /***********************************
571     *
572     *	Helper functions on searching (SQL)
573     *
574     ***********************************/
575    /**
576     * Returns a COMPLETE list of phash-integers matching the search-result composed of the search-words in the $searchWords array.
577     * The list of phash integers are unsorted and should be used for subsequent selection of index_phash records for display of the result.
578     *
579     * @param array $searchWords Search word array
580     * @return string List of integers
581     */
582    protected function getPhashList($searchWords)
583    {
584        // Initialize variables:
585        $c = 0;
586        // This array accumulates the phash-values
587        $totalHashList = [];
588        $this->wSelClauses = [];
589        // Traverse searchwords; for each, select all phash integers and merge/diff/intersect them with previous word (based on operator)
590        foreach ($searchWords as $k => $v) {
591            // Making the query for a single search word based on the search-type
592            $sWord = $v['sword'];
593            $theType = (string)$this->searchType;
594            // If there are spaces in the search-word, make a full text search instead.
595            if (strstr($sWord, ' ')) {
596                $theType = 20;
597            }
598            $this->getTimeTracker()->push('SearchWord "' . $sWord . '" - $theType=' . $theType);
599            // Perform search for word:
600            switch ($theType) {
601                case '1':
602                    // Part of word
603                    $res = $this->searchWord($sWord, Utility\LikeWildcard::BOTH);
604                    break;
605                case '2':
606                    // First part of word
607                    $res = $this->searchWord($sWord, Utility\LikeWildcard::RIGHT);
608                    break;
609                case '3':
610                    // Last part of word
611                    $res = $this->searchWord($sWord, Utility\LikeWildcard::LEFT);
612                    break;
613                case '10':
614                    // Sounds like
615                    /**
616                     * Indexer object
617                     *
618                     * @var Indexer
619                     */
620                    $indexerObj = GeneralUtility::makeInstance(Indexer::class);
621                    // Perform metaphone search
622                    $storeMetaphoneInfoAsWords = !$this->isTableUsed('index_words');
623                    $res = $this->searchMetaphone($indexerObj->metaphone($sWord, $storeMetaphoneInfoAsWords));
624                    unset($indexerObj);
625                    break;
626                case '20':
627                    // Sentence
628                    $res = $this->searchSentence($sWord);
629                    // If there is a fulltext search for a sentence there is
630                    // a likeliness that sorting cannot be done by the rankings
631                    // from the rel-table (because no relations will exist for the
632                    // sentence in the word-table). So therefore mtime is used instead.
633                    // It is not required, but otherwise some hits may be left out.
634                    $this->sortOrder = 'mtime';
635                    break;
636                default:
637                    // Distinct word
638                    $res = $this->searchDistinct($sWord);
639            }
640            // If there was a query to do, then select all phash-integers which resulted from this.
641            if ($res) {
642                // Get phash list by searching for it:
643                $phashList = [];
644                while ($row = $res->fetch()) {
645                    $phashList[] = $row['phash'];
646                }
647                // Here the phash list are merged with the existing result based on whether we are dealing with OR, NOT or AND operations.
648                if ($c) {
649                    switch ($v['oper']) {
650                        case 'OR':
651                            $totalHashList = array_unique(array_merge($phashList, $totalHashList));
652                            break;
653                        case 'AND NOT':
654                            $totalHashList = array_diff($totalHashList, $phashList);
655                            break;
656                        default:
657                            // AND...
658                            $totalHashList = array_intersect($totalHashList, $phashList);
659                    }
660                } else {
661                    // First search
662                    $totalHashList = $phashList;
663                }
664            }
665            $this->getTimeTracker()->pull();
666            $c++;
667        }
668        return implode(',', $totalHashList);
669    }
670
671    /**
672     * Returns a query which selects the search-word from the word/rel tables.
673     *
674     * @param string $wordSel WHERE clause selecting the word from phash
675     * @param string $additionalWhereClause Additional AND clause in the end of the query.
676     * @return Statement
677     */
678    protected function execPHashListQuery($wordSel, $additionalWhereClause = '')
679    {
680        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_words');
681        $queryBuilder->select('IR.phash')
682            ->from('index_words', 'IW')
683            ->from('index_rel', 'IR')
684            ->from('index_section', 'ISEC')
685            ->where(
686                QueryHelper::stripLogicalOperatorPrefix($wordSel),
687                $queryBuilder->expr()->eq('IW.wid', $queryBuilder->quoteIdentifier('IR.wid')),
688                $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IR.phash')),
689                QueryHelper::stripLogicalOperatorPrefix($this->sectionTableWhere()),
690                QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
691            )
692            ->groupBy('IR.phash');
693
694        return $queryBuilder->execute();
695    }
696
697    /**
698     * Search for a word
699     *
700     * @param string $sWord the search word
701     * @param int $wildcard Bit-field of Utility\LikeWildcard
702     * @return Statement
703     */
704    protected function searchWord($sWord, $wildcard)
705    {
706        $likeWildcard = Utility\LikeWildcard::cast($wildcard);
707        $wSel = $likeWildcard->getLikeQueryPart(
708            'index_words',
709            'IW.baseword',
710            $sWord
711        );
712        $this->wSelClauses[] = $wSel;
713        return $this->execPHashListQuery($wSel, ' AND is_stopword=0');
714    }
715
716    /**
717     * Search for one distinct word
718     *
719     * @param string $sWord the search word
720     * @return Statement
721     */
722    protected function searchDistinct($sWord)
723    {
724        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
725            ->getQueryBuilderForTable('index_words')
726            ->expr();
727        $wSel = $expressionBuilder->eq('IW.wid', $this->md5inthash($sWord));
728        $this->wSelClauses[] = $wSel;
729        return $this->execPHashListQuery($wSel, $expressionBuilder->eq('is_stopword', 0));
730    }
731
732    /**
733     * Search for a sentence
734     *
735     * @param string $sWord the search word
736     * @return Statement
737     */
738    protected function searchSentence($sWord)
739    {
740        $this->wSelClauses[] = '1=1';
741        $likeWildcard = Utility\LikeWildcard::cast(Utility\LikeWildcard::BOTH);
742        $likePart = $likeWildcard->getLikeQueryPart(
743            'index_fulltext',
744            'IFT.fulltextdata',
745            $sWord
746        );
747
748        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_section');
749        return $queryBuilder->select('ISEC.phash')
750            ->from('index_section', 'ISEC')
751            ->from('index_fulltext', 'IFT')
752            ->where(
753                QueryHelper::stripLogicalOperatorPrefix($likePart),
754                $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IFT.phash')),
755                QueryHelper::stripLogicalOperatorPrefix($this->sectionTableWhere())
756            )
757            ->groupBy('ISEC.phash')
758            ->execute();
759    }
760
761    /**
762     * Search for a metaphone word
763     *
764     * @param string $sWord the search word
765     * @return Statement
766     */
767    protected function searchMetaphone($sWord)
768    {
769        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
770            ->getQueryBuilderForTable('index_words')
771            ->expr();
772        $wSel = $expressionBuilder->eq('IW.metaphone', $expressionBuilder->literal($sWord));
773        $this->wSelClauses[] = $wSel;
774        return $this->execPHashListQuery($wSel, $expressionBuilder->eq('is_stopword', 0));
775    }
776
777    /**
778     * Returns AND statement for selection of section in database. (rootlevel 0-2 + page_id)
779     *
780     * @return string AND clause for selection of section in database.
781     */
782    public function sectionTableWhere()
783    {
784        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
785            ->getQueryBuilderForTable('index_section')
786            ->expr();
787
788        $whereClause = $expressionBuilder->andX();
789        $match = false;
790        if (!($this->searchRootPageIdList < 0)) {
791            $whereClause->add(
792                $expressionBuilder->in('ISEC.rl0', GeneralUtility::intExplode(',', $this->searchRootPageIdList, true))
793            );
794        }
795        if (strpos($this->sections, 'rl1_') === 0) {
796            $whereClause->add(
797                $expressionBuilder->in('ISEC.rl1', GeneralUtility::intExplode(',', substr($this->sections, 4)))
798            );
799            $match = true;
800        } elseif (strpos($this->sections, 'rl2_') === 0) {
801            $whereClause->add(
802                $expressionBuilder->in('ISEC.rl2', GeneralUtility::intExplode(',', substr($this->sections, 4)))
803            );
804            $match = true;
805        } else {
806            // Traversing user configured fields to see if any of those are used to limit search to a section:
807            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'] ?? [] as $fieldName => $rootLineLevel) {
808                if (strpos($this->sections, $fieldName . '_') === 0) {
809                    $whereClause->add(
810                        $expressionBuilder->in(
811                            'ISEC.' . $fieldName,
812                            GeneralUtility::intExplode(',', substr($this->sections, strlen($fieldName) + 1))
813                        )
814                    );
815                    $match = true;
816                    break;
817                }
818            }
819        }
820        // If no match above, test the static types:
821        if (!$match) {
822            switch ((string)$this->sections) {
823                case '-1':
824                    $whereClause->add(
825                        $expressionBuilder->eq('ISEC.page_id', (int)$this->getTypoScriptFrontendController()->id)
826                    );
827                    break;
828                case '-2':
829                    $whereClause->add($expressionBuilder->eq('ISEC.rl2', 0));
830                    break;
831                case '-3':
832                    $whereClause->add($expressionBuilder->gt('ISEC.rl2', 0));
833                    break;
834            }
835        }
836
837        return $whereClause->count() ? ' AND ' . $whereClause : '';
838    }
839
840    /**
841     * Returns AND statement for selection of media type
842     *
843     * @return string AND statement for selection of media type
844     */
845    public function mediaTypeWhere()
846    {
847        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
848            ->getQueryBuilderForTable('index_phash')
849            ->expr();
850        switch ($this->mediaType) {
851            case '0':
852                // '0' => 'only TYPO3 pages',
853                $whereClause = $expressionBuilder->eq('IP.item_type', $expressionBuilder->literal('0'));
854                break;
855            case '-2':
856                // All external documents
857                $whereClause = $expressionBuilder->neq('IP.item_type', $expressionBuilder->literal('0'));
858                break;
859            case false:
860                // Intentional fall-through
861            case '-1':
862                // All content
863                $whereClause = '';
864                break;
865            default:
866                $whereClause = $expressionBuilder->eq('IP.item_type', $expressionBuilder->literal($this->mediaType));
867        }
868        return $whereClause ? ' AND ' . $whereClause : '';
869    }
870
871    /**
872     * Returns AND statement for selection of language
873     *
874     * @return string AND statement for selection of language
875     */
876    public function languageWhere()
877    {
878        // -1 is the same as ALL language.
879        if ($this->languageUid < 0) {
880            return '';
881        }
882
883        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
884            ->getQueryBuilderForTable('index_phash')
885            ->expr();
886
887        return ' AND ' . $expressionBuilder->eq('IP.sys_language_uid', (int)$this->languageUid);
888    }
889
890    /**
891     * Where-clause for free index-uid value.
892     *
893     * @param int $freeIndexUid Free Index UID value to limit search to.
894     * @return string WHERE SQL clause part.
895     */
896    public function freeIndexUidWhere($freeIndexUid)
897    {
898        $freeIndexUid = (int)$freeIndexUid;
899        if ($freeIndexUid < 0) {
900            return '';
901        }
902        // First, look if the freeIndexUid is a meta configuration:
903        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
904            ->getQueryBuilderForTable('index_config');
905        $indexCfgRec = $queryBuilder->select('indexcfgs')
906            ->from('index_config')
907            ->where(
908                $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(5, \PDO::PARAM_INT)),
909                $queryBuilder->expr()->eq(
910                    'uid',
911                    $queryBuilder->createNamedParameter($freeIndexUid, \PDO::PARAM_INT)
912                )
913            )
914            ->execute()
915            ->fetch();
916
917        if (is_array($indexCfgRec)) {
918            $refs = GeneralUtility::trimExplode(',', $indexCfgRec['indexcfgs']);
919            // Default value to protect against empty array.
920            $list = [-99];
921            foreach ($refs as $ref) {
922                list($table, $uid) = GeneralUtility::revExplode('_', $ref, 2);
923                $uid = (int)$uid;
924                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
925                    ->getQueryBuilderForTable('index_config');
926                $queryBuilder->select('uid')
927                    ->from('index_config');
928                switch ($table) {
929                    case 'index_config':
930                        $idxRec = $queryBuilder
931                            ->where(
932                                $queryBuilder->expr()->eq(
933                                    'uid',
934                                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
935                                )
936                            )
937                            ->execute()
938                            ->fetch();
939                        if ($idxRec) {
940                            $list[] = $uid;
941                        }
942                        break;
943                    case 'pages':
944                        $indexCfgRecordsFromPid = $queryBuilder
945                            ->where(
946                                $queryBuilder->expr()->eq(
947                                    'pid',
948                                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
949                                )
950                            )
951                            ->execute();
952                        while ($idxRec = $indexCfgRecordsFromPid->fetch()) {
953                            $list[] = $idxRec['uid'];
954                        }
955                        break;
956                }
957            }
958            $list = array_unique($list);
959        } else {
960            $list = [$freeIndexUid];
961        }
962
963        $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
964            ->getQueryBuilderForTable('index_phash')
965            ->expr();
966        return ' AND ' . $expressionBuilder->in('IP.freeIndexUid', array_map('intval', $list));
967    }
968
969    /**
970     * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
971     *
972     * @param string $list List of phash integers which match the search.
973     * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
974     * @return Statement
975     */
976    protected function execFinalQuery($list, $freeIndexUid = -1)
977    {
978        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_words');
979        $queryBuilder->select('ISEC.*', 'IP.*')
980            ->from('index_phash', 'IP')
981            ->from('index_section', 'ISEC')
982            ->where(
983                $queryBuilder->expr()->in(
984                    'IP.phash',
985                    $queryBuilder->createNamedParameter(
986                        GeneralUtility::intExplode(',', $list, true),
987                        Connection::PARAM_INT_ARRAY
988                    )
989                ),
990                QueryHelper::stripLogicalOperatorPrefix($this->mediaTypeWhere()),
991                QueryHelper::stripLogicalOperatorPrefix($this->languageWhere()),
992                QueryHelper::stripLogicalOperatorPrefix($this->freeIndexUidWhere($freeIndexUid)),
993                $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IP.phash'))
994            )
995            ->groupBy(
996                'IP.phash',
997                'ISEC.phash',
998                'ISEC.phash_t3',
999                'ISEC.rl0',
1000                'ISEC.rl1',
1001                'ISEC.rl2',
1002                'ISEC.page_id',
1003                'ISEC.uniqid',
1004                'IP.phash_grouping',
1005                'IP.data_filename',
1006                'IP.data_page_id',
1007                // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Remove along with database field data_page_reg1
1008                'IP.data_page_reg1',
1009                'IP.data_page_type',
1010                'IP.data_page_mp',
1011                'IP.gr_list',
1012                'IP.item_type',
1013                'IP.item_title',
1014                'IP.item_description',
1015                'IP.item_mtime',
1016                'IP.tstamp',
1017                'IP.item_size',
1018                'IP.contentHash',
1019                'IP.crdate',
1020                'IP.parsetime',
1021                'IP.sys_language_uid',
1022                'IP.item_crdate',
1023                'IP.cHashParams',
1024                'IP.externalUrl',
1025                'IP.recordUid',
1026                'IP.freeIndexUid',
1027                'IP.freeIndexSetId',
1028                'IP.static_page_arguments'
1029            );
1030
1031        // Setting up methods of filtering results
1032        // based on page types, access, etc.
1033        if ($hookObj = $this->hookRequest('execFinalQuery_idList')) {
1034            // Calling hook for alternative creation of page ID list
1035            $hookWhere = QueryHelper::stripLogicalOperatorPrefix($hookObj->execFinalQuery_idList($list));
1036            if (!empty($hookWhere)) {
1037                $queryBuilder->andWhere($hookWhere);
1038            }
1039        } elseif ($this->joinPagesForQuery) {
1040            // Alternative to getting all page ids by ->getTreeList() where
1041            // "excludeSubpages" is NOT respected.
1042            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1043            $queryBuilder->from('pages');
1044            $queryBuilder->andWhere(
1045                $queryBuilder->expr()->eq('pages.uid', $queryBuilder->quoteIdentifier('ISEC.page_id')),
1046                $queryBuilder->expr()->eq(
1047                    'pages.no_search',
1048                    $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1049                ),
1050                $queryBuilder->expr()->lt(
1051                    'pages.doktype',
1052                    $queryBuilder->createNamedParameter(200, \PDO::PARAM_INT)
1053                )
1054            );
1055        } elseif ($this->searchRootPageIdList >= 0) {
1056            // Collecting all pages IDs in which to search;
1057            // filtering out ALL pages that are not accessible due to restriction containers.
1058            // Does NOT look for "no_search" field!
1059            $siteIdNumbers = GeneralUtility::intExplode(',', $this->searchRootPageIdList);
1060            $pageIdList = [];
1061            foreach ($siteIdNumbers as $rootId) {
1062                $pageIdList[] = $this->getTypoScriptFrontendController()->cObj->getTreeList(-1 * $rootId, 9999);
1063            }
1064            $queryBuilder->andWhere(
1065                $queryBuilder->expr()->in(
1066                    'ISEC.page_id',
1067                    $queryBuilder->createNamedParameter(
1068                        array_unique(GeneralUtility::intExplode(',', implode(',', $pageIdList), true)),
1069                        Connection::PARAM_INT_ARRAY
1070                    )
1071                )
1072            );
1073        }
1074        // otherwise select all / disable everything
1075        // If any of the ranking sortings are selected, we must make a
1076        // join with the word/rel-table again, because we need to
1077        // calculate ranking based on all search-words found.
1078        if (strpos($this->sortOrder, 'rank_') === 0) {
1079            $queryBuilder
1080                ->from('index_words', 'IW')
1081                ->from('index_rel', 'IR')
1082                ->andWhere(
1083                    $queryBuilder->expr()->eq('IW.wid', $queryBuilder->quoteIdentifier('IR.wid')),
1084                    $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IR.phash'))
1085                );
1086            switch ($this->sortOrder) {
1087                case 'rank_flag':
1088                    // This gives priority to word-position (max-value) so that words in title, keywords, description counts more than in content.
1089                    // The ordering is refined with the frequency sum as well.
1090                    $queryBuilder
1091                        ->addSelectLiteral(
1092                            $queryBuilder->expr()->max('IR.flags', 'order_val1'),
1093                            $queryBuilder->expr()->sum('IR.freq', 'order_val2')
1094                        )
1095                        ->orderBy('order_val1', $this->getDescendingSortOrderFlag())
1096                        ->addOrderBy('order_val2', $this->getDescendingSortOrderFlag());
1097                    break;
1098                case 'rank_first':
1099                    // Results in average position of search words on page.
1100                    // Must be inversely sorted (low numbers are closer to top)
1101                    $queryBuilder
1102                        ->addSelectLiteral($queryBuilder->expr()->avg('IR.first', 'order_val'))
1103                        ->orderBy('order_val', $this->getDescendingSortOrderFlag(true));
1104                    break;
1105                case 'rank_count':
1106                    // Number of words found
1107                    $queryBuilder
1108                        ->addSelectLiteral($queryBuilder->expr()->sum('IR.count', 'order_val'))
1109                        ->orderBy('order_val', $this->getDescendingSortOrderFlag());
1110                    break;
1111                default:
1112                    // Frequency sum. I'm not sure if this is the best way to do
1113                    // it (make a sum...). Or should it be the average?
1114                    $queryBuilder
1115                        ->addSelectLiteral($queryBuilder->expr()->sum('IR.freq', 'order_val'))
1116                        ->orderBy('order_val', $this->getDescendingSortOrderFlag());
1117            }
1118
1119            if (!empty($this->wSelClauses)) {
1120                // So, words are combined in an OR statement
1121                // (no "sentence search" should be done here - may deselect results)
1122                $wordSel = $queryBuilder->expr()->orX();
1123                foreach ($this->wSelClauses as $wSelClause) {
1124                    $wordSel->add(QueryHelper::stripLogicalOperatorPrefix($wSelClause));
1125                }
1126                $queryBuilder->andWhere($wordSel);
1127            }
1128        } else {
1129            // Otherwise, if sorting are done with the pages table or other fields,
1130            // there is no need for joining with the rel/word tables:
1131            switch ((string)$this->sortOrder) {
1132                case 'title':
1133                    $queryBuilder->orderBy('IP.item_title', $this->getDescendingSortOrderFlag());
1134                    break;
1135                case 'crdate':
1136                    $queryBuilder->orderBy('IP.item_crdate', $this->getDescendingSortOrderFlag());
1137                    break;
1138                case 'mtime':
1139                    $queryBuilder->orderBy('IP.item_mtime', $this->getDescendingSortOrderFlag());
1140                    break;
1141            }
1142        }
1143
1144        return $queryBuilder->execute();
1145    }
1146
1147    /**
1148     * Checking if the resume can be shown for the search result
1149     * (depending on whether the rights are OK)
1150     * ? Should it also check for gr_list "0,-1"?
1151     *
1152     * @param array $row Result row array.
1153     * @return bool Returns TRUE if resume can safely be shown
1154     */
1155    protected function checkResume($row)
1156    {
1157        // If the record is indexed by an indexing configuration, just show it.
1158        // At least this is needed for external URLs and files.
1159        // For records we might need to extend this - for instance block display if record is access restricted.
1160        if ($row['freeIndexUid']) {
1161            return true;
1162        }
1163        // Evaluate regularly indexed pages based on item_type:
1164        // External media:
1165        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('index_grlist');
1166        if ($row['item_type']) {
1167            // For external media we will check the access of the parent page on which the media was linked from.
1168            // "phash_t3" is the phash of the parent TYPO3 page row which initiated the indexing of the documents
1169            // in this section. So, selecting for the grlist records belonging to the parent phash-row where the
1170            // current users gr_list exists will help us to know. If this is NOT found, there is still a theoretical
1171            // possibility that another user accessible page would display a link, so maybe the resume of such a
1172            // document here may be unjustified hidden. But better safe than sorry.
1173            if (!$this->isTableUsed('index_grlist')) {
1174                return false;
1175            }
1176
1177            return (bool)$connection->count(
1178                'phash',
1179                'index_grlist',
1180                [
1181                    'phash' => (int)$row['phash_t3'],
1182                    'gr_list' => $this->frontendUserGroupList
1183                ]
1184            );
1185        }
1186        // Ordinary TYPO3 pages:
1187        if ((string)$row['gr_list'] !== (string)$this->frontendUserGroupList) {
1188            // Selecting for the grlist records belonging to the phash-row where the current users gr_list exists.
1189            // If it is found it is proof that this user has direct access to the phash-rows content although
1190            // he did not himself initiate the indexing...
1191            if (!$this->isTableUsed('index_grlist')) {
1192                return false;
1193            }
1194
1195            return (bool)$connection->count(
1196                'phash',
1197                'index_grlist',
1198                [
1199                        'phash' => (int)$row['phash'],
1200                        'gr_list' => $this->frontendUserGroupList
1201                    ]
1202            );
1203        }
1204        return true;
1205    }
1206
1207    /**
1208     * Returns "DESC" or "" depending on the settings of the incoming
1209     * highest/lowest result order (piVars['desc'])
1210     *
1211     * @param bool $inverse If TRUE, inverse the order which is defined by piVars['desc']
1212     * @return string " DESC" or formerly known as tx_indexedsearch_pi->isDescending
1213     */
1214    protected function getDescendingSortOrderFlag($inverse = false)
1215    {
1216        $desc = $this->descendingSortOrderFlag;
1217        if ($inverse) {
1218            $desc = !$desc;
1219        }
1220        return !$desc ? ' DESC' : '';
1221    }
1222
1223    /**
1224     * Returns if an item type is a multipage item type
1225     *
1226     * @param string $itemType Item type
1227     * @return bool TRUE if multipage capable
1228     */
1229    protected function multiplePagesType($itemType)
1230    {
1231        /** @var \TYPO3\CMS\IndexedSearch\FileContentParser $fileContentParser */
1232        $fileContentParser = $this->externalParsers[$itemType];
1233        return is_object($fileContentParser) && $fileContentParser->isMultiplePageExtension($itemType);
1234    }
1235
1236    /**
1237     * md5 integer hash
1238     * Using 7 instead of 8 just because that makes the integers lower than
1239     * 32 bit (28 bit) and so they do not interfere with UNSIGNED integers
1240     * or PHP-versions which has varying output from the hexdec function.
1241     *
1242     * @param string $str String to hash
1243     * @return int Integer interpretation of the md5 hash of input string.
1244     */
1245    protected function md5inthash($str)
1246    {
1247        return Utility\IndexedSearchUtility::md5inthash($str);
1248    }
1249
1250    /**
1251     * Check if the tables provided are configured for usage.
1252     * This becomes necessary for extensions that provide additional database
1253     * functionality like indexed_search_mysql.
1254     *
1255     * @param string $table_list Comma-separated list of tables
1256     * @return bool TRUE if given tables are enabled
1257     */
1258    protected function isTableUsed($table_list)
1259    {
1260        return Utility\IndexedSearchUtility::isTableUsed($table_list);
1261    }
1262
1263    /**
1264     * Returns an object reference to the hook object if any
1265     *
1266     * @param string $functionName Name of the function you want to call / hook key
1267     * @return object|null Hook object, if any. Otherwise NULL.
1268     */
1269    public function hookRequest($functionName)
1270    {
1271        // Hook: menuConfig_preProcessModMenu
1272        if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
1273            $hookObj = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
1274            if (method_exists($hookObj, $functionName)) {
1275                $hookObj->pObj = $this;
1276                return $hookObj;
1277            }
1278        }
1279        return null;
1280    }
1281
1282    /**
1283     * Search type
1284     * e.g. sentence (20), any part of the word (1)
1285     *
1286     * @return int
1287     */
1288    public function getSearchType()
1289    {
1290        return (int)$this->searchType;
1291    }
1292
1293    /**
1294     * A list of integer which should be root-pages to search from
1295     *
1296     * @return int[]
1297     */
1298    public function getSearchRootPageIdList()
1299    {
1300        return GeneralUtility::intExplode(',', $this->searchRootPageIdList);
1301    }
1302
1303    /**
1304     * Getter for joinPagesForQuery flag
1305     * enabled through TypoScript 'settings.skipExtendToSubpagesChecking'
1306     *
1307     * @return bool
1308     */
1309    public function getJoinPagesForQuery()
1310    {
1311        return $this->joinPagesForQuery;
1312    }
1313
1314    /**
1315     * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
1316     */
1317    protected function getTypoScriptFrontendController()
1318    {
1319        return $GLOBALS['TSFE'];
1320    }
1321
1322    /**
1323     * @return TimeTracker
1324     */
1325    protected function getTimeTracker()
1326    {
1327        return GeneralUtility::makeInstance(TimeTracker::class);
1328    }
1329}
1330