d28289a6fb12625bc39832637834d1277811712d
[Packages/TYPO3.CMS.git] / typo3 / sysext / indexed_search / Classes / Domain / Repository / IndexSearchRepository.php
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;
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;
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 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();
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));
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()) {
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();
295
296 return [
297 'resultRows' => $resultRows,
298 'firstRow' => $firstRow,
299 'count' => $count
300 ];
301 } else {
302 // No results found
303 return false;
304 }
305 }
306
307 /**
308 * Gets a SQL result pointer to traverse for the search records.
309 *
310 * @param array $searchWords Search words
311 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
312 * @return Statement
313 */
314 protected function getResultRows_SQLpointer($searchWords, $freeIndexUid = -1)
315 {
316 // This SEARCHES for the searchwords in $searchWords AND returns a
317 // COMPLETE list of phash-integers of the matches.
318 $list = $this->getPhashList($searchWords);
319 // Perform SQL Search / collection of result rows array:
320 if ($list) {
321 // Do the search:
322 $this->getTimeTracker()->push('execFinalQuery');
323 $res = $this->execFinalQuery($list, $freeIndexUid);
324 $this->getTimeTracker()->pull();
325 return $res;
326 } else {
327 return false;
328 }
329 }
330
331 /**
332 * Gets a SQL result pointer to traverse for the search records.
333 *
334 * mysql fulltext specific version triggered by ext_conf_template setting 'useMysqlFulltext'
335 *
336 * @param array $searchWordsArray Search words
337 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
338 * @return bool|\mysqli_result|object MySQLi result object / DBAL object
339 */
340 protected function getResultRows_SQLpointerMysqlFulltext($searchWordsArray, $freeIndexUid = -1)
341 {
342 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('index_fulltext');
343 if (strpos($connection->getServerVersion(), 'MySQL') !== 0) {
344 throw new \RuntimeException(
345 'Extension indexed_search is configured to use mysql fulltext, but table \'index_fulltext\''
346 . ' is running on a different DBMS.',
347 1472585525
348 );
349 }
350 // Build the search string, detect which fulltext index to use, and decide whether boolean search is needed or not
351 $searchData = $this->getSearchString($searchWordsArray);
352 // Perform SQL Search / collection of result rows array:
353 $resource = false;
354 if ($searchData) {
355 /** @var TimeTracker $timeTracker */
356 $timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
357 // Do the search:
358 $timeTracker->push('execFinalQuery');
359 $resource = $this->execFinalQuery_fulltext($searchData, $freeIndexUid);
360 $timeTracker->pull();
361 }
362 return $resource;
363 }
364
365 /**
366 * Returns a search string for use with MySQL FULLTEXT query
367 *
368 * mysql fulltext specific helper method
369 *
370 * @param array $searchWordArray Search word array
371 * @return string Search string
372 */
373 protected function getSearchString($searchWordArray)
374 {
375 // Initialize variables:
376 $count = 0;
377 // Change this to TRUE to force BOOLEAN SEARCH MODE (useful if fulltext index is still empty)
378 $searchBoolean = false;
379 $fulltextIndex = 'index_fulltext.fulltextdata';
380 // This holds the result if the search is natural (doesn't contain any boolean operators)
381 $naturalSearchString = '';
382 // This holds the result if the search is boolen (contains +/-/| operators)
383 $booleanSearchString = '';
384
385 $searchType = (string)$this->getSearchType();
386
387 // Traverse searchwords and prefix them with corresponding operator
388 foreach ($searchWordArray as $searchWordData) {
389 // Making the query for a single search word based on the search-type
390 $searchWord = $searchWordData['sword'];
391 $wildcard = '';
392 if (strstr($searchWord, ' ')) {
393 $searchType = '20';
394 }
395 switch ($searchType) {
396 case '1':
397 case '2':
398 case '3':
399 // First part of word
400 $wildcard = '*';
401 // Part-of-word search requires boolean mode!
402 $searchBoolean = true;
403 break;
404 case '10':
405 $indexerObj = GeneralUtility::makeInstance(Indexer::class);
406 // Initialize the indexer-class
407 /** @var Indexer $indexerObj */
408 $searchWord = $indexerObj->metaphone($searchWord, $indexerObj->storeMetaphoneInfoAsWords);
409 unset($indexerObj);
410 $fulltextIndex = 'index_fulltext.metaphonedata';
411 break;
412 case '20':
413 $searchBoolean = true;
414 // Remove existing quotes and fix misplaced quotes.
415 $searchWord = trim(str_replace('"', ' ', $searchWord));
416 break;
417 }
418 // Perform search for word:
419 switch ($searchWordData['oper']) {
420 case 'AND NOT':
421 $booleanSearchString .= ' -' . $searchWord . $wildcard;
422 $searchBoolean = true;
423 break;
424 case 'OR':
425 $booleanSearchString .= ' ' . $searchWord . $wildcard;
426 $searchBoolean = true;
427 break;
428 default:
429 $booleanSearchString .= ' +' . $searchWord . $wildcard;
430 $naturalSearchString .= ' ' . $searchWord;
431 }
432 $count++;
433 }
434 if ($searchType == '20') {
435 $searchString = '"' . trim($naturalSearchString) . '"';
436 } elseif ($searchBoolean) {
437 $searchString = trim($booleanSearchString);
438 } else {
439 $searchString = trim($naturalSearchString);
440 }
441 return [
442 'searchBoolean' => $searchBoolean,
443 'searchString' => $searchString,
444 'fulltextIndex' => $fulltextIndex
445 ];
446 }
447
448 /**
449 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
450 *
451 * mysql fulltext specific helper method
452 *
453 * @param array $searchData Array with search string, boolean indicator, and fulltext index reference
454 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
455 * @return Statement
456 */
457 protected function execFinalQuery_fulltext($searchData, $freeIndexUid = -1)
458 {
459 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_fulltext');
460 $queryBuilder->getRestrictions()->removeAll();
461 $queryBuilder->select('index_fulltext.*', 'ISEC.*', 'IP.*')
462 ->from('index_fulltext')
463 ->join(
464 'index_fulltext',
465 'index_phash',
466 'IP',
467 $queryBuilder->expr()->eq('index_fulltext.phash', $queryBuilder->quoteIdentifier('IP.phash'))
468 )
469 ->join(
470 'IP',
471 'index_section',
472 'ISEC',
473 $queryBuilder->expr()->eq('IP.phash', $queryBuilder->quoteIdentifier('ISEC.phash'))
474 );
475
476 // Calling hook for alternative creation of page ID list
477 $searchRootPageIdList = $this->getSearchRootPageIdList();
478 if ($hookObj = &$this->hookRequest('execFinalQuery_idList')) {
479 $pageWhere = $hookObj->execFinalQuery_idList('');
480 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($pageWhere));
481 } elseif ($this->getJoinPagesForQuery()) {
482 // Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
483 $queryBuilder
484 ->join(
485 'ISEC',
486 'pages',
487 'pages',
488 $queryBuilder->expr()->eq('ISEC.page_id', $queryBuilder->quoteIdentifier('pages.uid'))
489 )
490 ->andWhere(
491 $queryBuilder->expr()->eq(
492 'pages.no_search',
493 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
494 )
495 )
496 ->andWhere(
497 $queryBuilder->expr()->lt(
498 'pages.doktype',
499 $queryBuilder->createNamedParameter(200, \PDO::PARAM_INT)
500 )
501 );
502 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
503 } elseif ($searchRootPageIdList[0] >= 0) {
504 // Collecting all pages IDs in which to search;
505 // filtering out ALL pages that are not accessible due to enableFields. Does NOT look for "no_search" field!
506 $idList = [];
507 foreach ($searchRootPageIdList as $rootId) {
508 /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
509 $cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
510 $idList[] = $cObj->getTreeList(-1 * $rootId, 9999);
511 }
512 $idList = GeneralUtility::intExplode(',', implode(',', $idList));
513 $queryBuilder->andWhere(
514 $queryBuilder->expr()->in(
515 'ISEC.page_id',
516 $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY)
517 )
518 );
519 }
520
521 $searchBoolean = '';
522 if ($searchData['searchBoolean']) {
523 $searchBoolean = ' IN BOOLEAN MODE';
524 }
525 $queryBuilder->andWhere(
526 'MATCH (' . $queryBuilder->quoteIdentifier($searchData['fulltextIndex']) . ')'
527 . ' AGAINST (' . $queryBuilder->createNamedParameter($searchData['searchString'])
528 . $searchBoolean
529 . ')'
530 );
531
532 $queryBuilder->andWhere(
533 QueryHelper::stripLogicalOperatorPrefix($this->mediaTypeWhere()),
534 QueryHelper::stripLogicalOperatorPrefix($this->languageWhere()),
535 QueryHelper::stripLogicalOperatorPrefix($this->freeIndexUidWhere($freeIndexUid)),
536 QueryHelper::stripLogicalOperatorPrefix($this->sectionTableWhere())
537 );
538
539 $queryBuilder->groupBy(
540 'IP.phash',
541 'ISEC.phash',
542 'ISEC.phash_t3',
543 'ISEC.rl0',
544 'ISEC.rl1',
545 'ISEC.rl2',
546 'ISEC.page_id',
547 'ISEC.uniqid',
548 'IP.phash_grouping',
549 'IP.data_filename',
550 'IP.data_page_id',
551 'IP.data_page_reg1',
552 'IP.data_page_type',
553 'IP.data_page_mp',
554 'IP.gr_list',
555 'IP.item_type',
556 'IP.item_title',
557 'IP.item_description',
558 'IP.item_mtime',
559 'IP.tstamp',
560 'IP.item_size',
561 'IP.contentHash',
562 'IP.crdate',
563 'IP.parsetime',
564 'IP.sys_language_uid',
565 'IP.item_crdate',
566 'IP.cHashParams',
567 'IP.externalUrl',
568 'IP.recordUid',
569 'IP.freeIndexUid',
570 'IP.freeIndexSetId'
571 );
572
573 return $queryBuilder->execute();
574 }
575
576 /***********************************
577 *
578 * Helper functions on searching (SQL)
579 *
580 ***********************************/
581 /**
582 * Returns a COMPLETE list of phash-integers matching the search-result composed of the search-words in the $searchWords array.
583 * The list of phash integers are unsorted and should be used for subsequent selection of index_phash records for display of the result.
584 *
585 * @param array $searchWords Search word array
586 * @return string List of integers
587 */
588 protected function getPhashList($searchWords)
589 {
590 // Initialize variables:
591 $c = 0;
592 // This array accumulates the phash-values
593 $totalHashList = [];
594 $this->wSelClauses = [];
595 // Traverse searchwords; for each, select all phash integers and merge/diff/intersect them with previous word (based on operator)
596 foreach ($searchWords as $k => $v) {
597 // Making the query for a single search word based on the search-type
598 $sWord = $v['sword'];
599 $theType = (string)$this->searchType;
600 // If there are spaces in the search-word, make a full text search instead.
601 if (strstr($sWord, ' ')) {
602 $theType = 20;
603 }
604 $this->getTimeTracker()->push('SearchWord "' . $sWord . '" - $theType=' . $theType);
605 // Perform search for word:
606 switch ($theType) {
607 case '1':
608 // Part of word
609 $res = $this->searchWord($sWord, Utility\LikeWildcard::BOTH);
610 break;
611 case '2':
612 // First part of word
613 $res = $this->searchWord($sWord, Utility\LikeWildcard::RIGHT);
614 break;
615 case '3':
616 // Last part of word
617 $res = $this->searchWord($sWord, Utility\LikeWildcard::LEFT);
618 break;
619 case '10':
620 // Sounds like
621 /**
622 * Indexer object
623 *
624 * @var Indexer
625 */
626 $indexerObj = GeneralUtility::makeInstance(Indexer::class);
627 // Perform metaphone search
628 $storeMetaphoneInfoAsWords = !$this->isTableUsed('index_words');
629 $res = $this->searchMetaphone($indexerObj->metaphone($sWord, $storeMetaphoneInfoAsWords));
630 unset($indexerObj);
631 break;
632 case '20':
633 // Sentence
634 $res = $this->searchSentence($sWord);
635 // If there is a fulltext search for a sentence there is
636 // a likeliness that sorting cannot be done by the rankings
637 // from the rel-table (because no relations will exist for the
638 // sentence in the word-table). So therefore mtime is used instead.
639 // It is not required, but otherwise some hits may be left out.
640 $this->sortOrder = 'mtime';
641 break;
642 default:
643 // Distinct word
644 $res = $this->searchDistinct($sWord);
645 }
646 // If there was a query to do, then select all phash-integers which resulted from this.
647 if ($res) {
648 // Get phash list by searching for it:
649 $phashList = [];
650 while ($row = $res->fetch()) {
651 $phashList[] = $row['phash'];
652 }
653 // Here the phash list are merged with the existing result based on whether we are dealing with OR, NOT or AND operations.
654 if ($c) {
655 switch ($v['oper']) {
656 case 'OR':
657 $totalHashList = array_unique(array_merge($phashList, $totalHashList));
658 break;
659 case 'AND NOT':
660 $totalHashList = array_diff($totalHashList, $phashList);
661 break;
662 default:
663 // AND...
664 $totalHashList = array_intersect($totalHashList, $phashList);
665 }
666 } else {
667 // First search
668 $totalHashList = $phashList;
669 }
670 }
671 $this->getTimeTracker()->pull();
672 $c++;
673 }
674 return implode(',', $totalHashList);
675 }
676
677 /**
678 * Returns a query which selects the search-word from the word/rel tables.
679 *
680 * @param string $wordSel WHERE clause selecting the word from phash
681 * @param string $additionalWhereClause Additional AND clause in the end of the query.
682 * @return Statement
683 */
684 protected function execPHashListQuery($wordSel, $additionalWhereClause = '')
685 {
686 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_words');
687 $queryBuilder->select('IR.phash')
688 ->from('index_words', 'IW')
689 ->from('index_rel', 'IR')
690 ->from('index_section', 'ISEC')
691 ->where(
692 QueryHelper::stripLogicalOperatorPrefix($wordSel),
693 $queryBuilder->expr()->eq('IW.wid', $queryBuilder->quoteIdentifier('IR.wid')),
694 $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IR.phash')),
695 QueryHelper::stripLogicalOperatorPrefix($this->sectionTableWhere()),
696 QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
697 )
698 ->groupBy('IR.phash');
699
700 return $queryBuilder->execute();
701 }
702
703 /**
704 * Search for a word
705 *
706 * @param string $sWord the search word
707 * @param int $wildcard Bit-field of Utility\LikeWildcard
708 * @return Statement
709 */
710 protected function searchWord($sWord, $wildcard)
711 {
712 $likeWildcard = Utility\LikeWildcard::cast($wildcard);
713 $wSel = $likeWildcard->getLikeQueryPart(
714 'index_words',
715 'IW.baseword',
716 $sWord
717 );
718 $this->wSelClauses[] = $wSel;
719 return $this->execPHashListQuery($wSel, ' AND is_stopword=0');
720 }
721
722 /**
723 * Search for one distinct word
724 *
725 * @param string $sWord the search word
726 * @return Statement
727 */
728 protected function searchDistinct($sWord)
729 {
730 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
731 ->getQueryBuilderForTable('index_words')
732 ->expr();
733 $wSel = $expressionBuilder->eq('IW.wid', $this->md5inthash($sWord));
734 $this->wSelClauses[] = $wSel;
735 return $this->execPHashListQuery($wSel, $expressionBuilder->eq('is_stopword', 0));
736 }
737
738 /**
739 * Search for a sentence
740 *
741 * @param string $sWord the search word
742 * @return Statement
743 */
744 protected function searchSentence($sWord)
745 {
746 $this->wSelClauses[] = '1=1';
747 $likeWildcard = Utility\LikeWildcard::cast(Utility\LikeWildcard::BOTH);
748 $likePart = $likeWildcard->getLikeQueryPart(
749 'index_fulltext',
750 'IFT.fulltextdata',
751 $sWord
752 );
753
754 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_section');
755 return $queryBuilder->select('ISEC.phash')
756 ->from('index_section', 'ISEC')
757 ->from('index_fulltext', 'IFT')
758 ->where(
759 QueryHelper::stripLogicalOperatorPrefix($likePart),
760 $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier(('IFT.phash'))),
761 QueryHelper::stripLogicalOperatorPrefix($this->sectionTableWhere())
762 )
763 ->groupBy('ISEC.phash')
764 ->execute();
765 }
766
767 /**
768 * Search for a metaphone word
769 *
770 * @param string $sWord the search word
771 * @return Statement
772 */
773 protected function searchMetaphone($sWord)
774 {
775 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
776 ->getQueryBuilderForTable('index_words')
777 ->expr();
778 $wSel = $expressionBuilder->eq('IW.metaphone', $expressionBuilder->literal($sWord));
779 $this->wSelClauses[] = $wSel;
780 return $this->execPHashListQuery($wSel, $expressionBuilder->eq('is_stopword', 0));
781 }
782
783 /**
784 * Returns AND statement for selection of section in database. (rootlevel 0-2 + page_id)
785 *
786 * @return string AND clause for selection of section in database.
787 */
788 public function sectionTableWhere()
789 {
790 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
791 ->getQueryBuilderForTable('index_section')
792 ->expr();
793
794 $whereClause = $expressionBuilder->andX();
795 $match = false;
796 if (!($this->searchRootPageIdList < 0)) {
797 $whereClause->add(
798 $expressionBuilder->in('ISEC.rl0', GeneralUtility::intExplode(',', $this->searchRootPageIdList, true))
799 );
800 }
801 if (substr($this->sections, 0, 4) === 'rl1_') {
802 $whereClause->add(
803 $expressionBuilder->in('ISEC.rl1', GeneralUtility::intExplode(',', substr($this->sections, 4)))
804 );
805 $match = true;
806 } elseif (substr($this->sections, 0, 4) === 'rl2_') {
807 $whereClause->add(
808 $expressionBuilder->in('ISEC.rl2', GeneralUtility::intExplode(',', substr($this->sections, 4)))
809 );
810 $match = true;
811 } elseif (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'])) {
812 // Traversing user configured fields to see if any of those are used to limit search to a section:
813 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'] as $fieldName => $rootLineLevel) {
814 if (substr($this->sections, 0, strlen($fieldName) + 1) == $fieldName . '_') {
815 $whereClause->add(
816 $expressionBuilder->in(
817 'ISEC.' . $fieldName,
818 GeneralUtility::intExplode(',', substr($this->sections, strlen($fieldName) + 1))
819 )
820 );
821 $match = true;
822 break;
823 }
824 }
825 }
826 // If no match above, test the static types:
827 if (!$match) {
828 switch ((string)$this->sections) {
829 case '-1':
830 $whereClause->add(
831 $expressionBuilder->eq('ISEC.page_id', (int)$this->getTypoScriptFrontendController()->id)
832 );
833 break;
834 case '-2':
835 $whereClause->add($expressionBuilder->eq('ISEC.rl2', 0));
836 break;
837 case '-3':
838 $whereClause->add($expressionBuilder->gt('ISEC.rl2', 0));
839 break;
840 }
841 }
842
843 return $whereClause->count() ? ' AND ' . $whereClause : '';
844 }
845
846 /**
847 * Returns AND statement for selection of media type
848 *
849 * @return string AND statement for selection of media type
850 */
851 public function mediaTypeWhere()
852 {
853 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
854 ->getQueryBuilderForTable('index_phash')
855 ->expr();
856 switch ($this->mediaType) {
857 case '0':
858 // '0' => 'only TYPO3 pages',
859 $whereClause = $expressionBuilder->eq('IP.item_type', $expressionBuilder->literal('0'));
860 break;
861 case '-2':
862 // All external documents
863 $whereClause = $expressionBuilder->neq('IP.item_type', $expressionBuilder->literal('0'));
864 break;
865 case false:
866 // Intentional fall-through
867 case '-1':
868 // All content
869 $whereClause = '';
870 break;
871 default:
872 $whereClause = $expressionBuilder->eq('IP.item_type', $expressionBuilder->literal($this->mediaType));
873 }
874 return $whereClause ? ' AND ' . $whereClause : '';
875 }
876
877 /**
878 * Returns AND statement for selection of language
879 *
880 * @return string AND statement for selection of language
881 */
882 public function languageWhere()
883 {
884 // -1 is the same as ALL language.
885 if ($this->languageUid < 0) {
886 return '';
887 }
888
889 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
890 ->getQueryBuilderForTable('index_phash')
891 ->expr();
892
893 return ' AND ' . $expressionBuilder->eq('IP.sys_language_uid', (int)$this->languageUid);
894 }
895
896 /**
897 * Where-clause for free index-uid value.
898 *
899 * @param int $freeIndexUid Free Index UID value to limit search to.
900 * @return string WHERE SQL clause part.
901 */
902 public function freeIndexUidWhere($freeIndexUid)
903 {
904 $freeIndexUid = (int)$freeIndexUid;
905 if ($freeIndexUid < 0) {
906 return '';
907 }
908 // First, look if the freeIndexUid is a meta configuration:
909 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
910 ->getQueryBuilderForTable('index_config');
911 $queryBuilder->getRestrictions()->removeAll();
912 $indexCfgRec = $queryBuilder->select('indexcfgs')
913 ->from('index_config')
914 ->where(
915 $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(5, \PDO::PARAM_INT)),
916 $queryBuilder->expr()->eq(
917 'uid',
918 $queryBuilder->createNamedParameter($freeIndexUid, \PDO::PARAM_INT)
919 ),
920 QueryHelper::stripLogicalOperatorPrefix($this->enableFields('index_config'))
921 )
922 ->execute()
923 ->fetch();
924
925 if (is_array($indexCfgRec)) {
926 $refs = GeneralUtility::trimExplode(',', $indexCfgRec['indexcfgs']);
927 // Default value to protect against empty array.
928 $list = [-99];
929 foreach ($refs as $ref) {
930 list($table, $uid) = GeneralUtility::revExplode('_', $ref, 2);
931 $uid = (int)$uid;
932 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
933 ->getQueryBuilderForTable('index_config');
934 $queryBuilder->getRestrictions()->removeAll();
935 $queryBuilder->select('uid')
936 ->from('index_config')
937 ->where(QueryHelper::stripLogicalOperatorPrefix($this->enableFields('index_config')));
938 switch ($table) {
939 case 'index_config':
940 $idxRec = $queryBuilder
941 ->andWhere(
942 $queryBuilder->expr()->eq(
943 'uid',
944 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
945 )
946 )
947 ->execute()
948 ->fetch();
949 if ($idxRec) {
950 $list[] = $uid;
951 }
952 break;
953 case 'pages':
954 $indexCfgRecordsFromPid = $queryBuilder
955 ->andWhere(
956 $queryBuilder->expr()->eq(
957 'pid',
958 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
959 )
960 )
961 ->execute();
962 while ($idxRec = $indexCfgRecordsFromPid->fetch()) {
963 $list[] = $idxRec['uid'];
964 }
965 break;
966 }
967 }
968 $list = array_unique($list);
969 } else {
970 $list = [$freeIndexUid];
971 }
972
973 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
974 ->getQueryBuilderForTable('index_phash')
975 ->expr();
976 return ' AND ' . $expressionBuilder->in('IP.freeIndexUid', array_map('intval', $list));
977 }
978
979 /**
980 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
981 *
982 * @param string $list List of phash integers which match the search.
983 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
984 * @return Statement
985 */
986 protected function execFinalQuery($list, $freeIndexUid = -1)
987 {
988 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_words');
989 $queryBuilder->select('ISEC.*', 'IP.*')
990 ->from('index_phash', 'IP')
991 ->from('index_section', 'ISEC')
992 ->where(
993 $queryBuilder->expr()->in(
994 'IP.phash',
995 $queryBuilder->createNamedParameter(
996 GeneralUtility::intExplode(',', $list, true),
997 Connection::PARAM_INT_ARRAY
998 )
999 ),
1000 QueryHelper::stripLogicalOperatorPrefix($this->mediaTypeWhere()),
1001 QueryHelper::stripLogicalOperatorPrefix($this->languageWhere()),
1002 QueryHelper::stripLogicalOperatorPrefix($this->freeIndexUidWhere($freeIndexUid)),
1003 $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IP.phash'))
1004 )
1005 ->groupBy(
1006 'IP.phash',
1007 'ISEC.phash',
1008 'ISEC.phash_t3',
1009 'ISEC.rl0',
1010 'ISEC.rl1',
1011 'ISEC.rl2',
1012 'ISEC.page_id',
1013 'ISEC.uniqid',
1014 'IP.phash_grouping',
1015 'IP.data_filename',
1016 'IP.data_page_id',
1017 'IP.data_page_reg1',
1018 'IP.data_page_type',
1019 'IP.data_page_mp',
1020 'IP.gr_list',
1021 'IP.item_type',
1022 'IP.item_title',
1023 'IP.item_description',
1024 'IP.item_mtime',
1025 'IP.tstamp',
1026 'IP.item_size',
1027 'IP.contentHash',
1028 'IP.crdate',
1029 'IP.parsetime',
1030 'IP.sys_language_uid',
1031 'IP.item_crdate',
1032 'IP.cHashParams',
1033 'IP.externalUrl',
1034 'IP.recordUid',
1035 'IP.freeIndexUid',
1036 'IP.freeIndexSetId'
1037 );
1038
1039 // Setting up methods of filtering results
1040 // based on page types, access, etc.
1041 if ($hookObj = $this->hookRequest('execFinalQuery_idList')) {
1042 // Calling hook for alternative creation of page ID list
1043 $hookWhere = QueryHelper::stripLogicalOperatorPrefix($hookObj->execFinalQuery_idList($list));
1044 if (!empty($hookWhere)) {
1045 $queryBuilder->andWhere($hookWhere);
1046 }
1047 } elseif ($this->joinPagesForQuery) {
1048 // Alternative to getting all page ids by ->getTreeList() where
1049 // "excludeSubpages" is NOT respected.
1050 $queryBuilder->getRestrictions()->removeAll();
1051 $queryBuilder->from('pages');
1052 $queryBuilder->andWhere(
1053 $queryBuilder->expr()->eq('pages.uid', $queryBuilder->quoteIdentifier('ISEC.page_id')),
1054 QueryHelper::stripLogicalOperatorPrefix($this->enableFields('pages')),
1055 $queryBuilder->expr()->eq(
1056 'pages.no_search',
1057 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1058 ),
1059 $queryBuilder->expr()->lt(
1060 'pages.doktype',
1061 $queryBuilder->createNamedParameter(200, \PDO::PARAM_INT)
1062 )
1063 );
1064 } elseif ($this->searchRootPageIdList >= 0) {
1065 // Collecting all pages IDs in which to search;
1066 // filtering out ALL pages that are not accessible due to enableFields.
1067 // Does NOT look for "no_search" field!
1068 $siteIdNumbers = GeneralUtility::intExplode(',', $this->searchRootPageIdList);
1069 $pageIdList = [];
1070 foreach ($siteIdNumbers as $rootId) {
1071 $pageIdList[] = $this->getTypoScriptFrontendController()->cObj->getTreeList(-1 * $rootId, 9999);
1072 }
1073 $queryBuilder->andWhere(
1074 $queryBuilder->expr()->in(
1075 'ISEC.page_id',
1076 $queryBuilder->createNamedParameter(
1077 array_unique(GeneralUtility::intExplode(',', implode(',', $pageIdList), true)),
1078 Connection::PARAM_INT_ARRAY
1079 )
1080 )
1081 );
1082 }
1083 // otherwise select all / disable everything
1084 // If any of the ranking sortings are selected, we must make a
1085 // join with the word/rel-table again, because we need to
1086 // calculate ranking based on all search-words found.
1087 if (substr($this->sortOrder, 0, 5) === 'rank_') {
1088 $queryBuilder
1089 ->from('index_words', 'IW')
1090 ->from('index_rel', 'IR')
1091 ->andWhere(
1092 $queryBuilder->expr()->eq('IW.wid', $queryBuilder->quoteIdentifier('IR.wid')),
1093 $queryBuilder->expr()->eq('ISEC.phash', $queryBuilder->quoteIdentifier('IR.phash'))
1094 );
1095 switch ($this->sortOrder) {
1096 case 'rank_flag':
1097 // This gives priority to word-position (max-value) so that words in title, keywords, description counts more than in content.
1098 // The ordering is refined with the frequency sum as well.
1099 $queryBuilder
1100 ->addSelectLiteral(
1101 $queryBuilder->expr()->max('IR.flags', 'order_val1'),
1102 $queryBuilder->expr()->sum('IR.freq', 'order_val2')
1103 )
1104 ->orderBy('order_val1', $this->getDescendingSortOrderFlag())
1105 ->addOrderBy('order_val2', $this->getDescendingSortOrderFlag());
1106 break;
1107 case 'rank_first':
1108 // Results in average position of search words on page.
1109 // Must be inversely sorted (low numbers are closer to top)
1110 $queryBuilder
1111 ->addSelectLiteral($queryBuilder->expr()->avg('IR.first', 'order_val'))
1112 ->orderBy('order_val', $this->getDescendingSortOrderFlag(true));
1113 break;
1114 case 'rank_count':
1115 // Number of words found
1116 $queryBuilder
1117 ->addSelectLiteral($queryBuilder->expr()->sum('IR.count', 'order_val'))
1118 ->orderBy('order_val', $this->getDescendingSortOrderFlag());
1119 break;
1120 default:
1121 // Frequency sum. I'm not sure if this is the best way to do
1122 // it (make a sum...). Or should it be the average?
1123 $queryBuilder
1124 ->addSelectLiteral($queryBuilder->expr()->sum('IR.freq', 'order_val'))
1125 ->orderBy('order_val', $this->getDescendingSortOrderFlag());
1126 }
1127
1128 if (!empty($this->wSelClauses)) {
1129 // So, words are combined in an OR statement
1130 // (no "sentence search" should be done here - may deselect results)
1131 $wordSel = $queryBuilder->expr()->orX();
1132 foreach ($this->wSelClauses as $wSelClause) {
1133 $wordSel->add(QueryHelper::stripLogicalOperatorPrefix($wSelClause));
1134 }
1135 $queryBuilder->andWhere($wordSel);
1136 }
1137 } else {
1138 // Otherwise, if sorting are done with the pages table or other fields,
1139 // there is no need for joining with the rel/word tables:
1140 switch ((string)$this->sortOrder) {
1141 case 'title':
1142 $queryBuilder->orderBy('IP.item_title', $this->getDescendingSortOrderFlag());
1143 break;
1144 case 'crdate':
1145 $queryBuilder->orderBy('IP.item_crdate', $this->getDescendingSortOrderFlag());
1146 break;
1147 case 'mtime':
1148 $queryBuilder->orderBy('IP.item_mtime', $this->getDescendingSortOrderFlag());
1149 break;
1150 }
1151 }
1152
1153 return $queryBuilder->execute();
1154 }
1155
1156 /**
1157 * Checking if the resume can be shown for the search result
1158 * (depending on whether the rights are OK)
1159 * ? Should it also check for gr_list "0,-1"?
1160 *
1161 * @param array $row Result row array.
1162 * @return bool Returns TRUE if resume can safely be shown
1163 */
1164 protected function checkResume($row)
1165 {
1166 // If the record is indexed by an indexing configuration, just show it.
1167 // At least this is needed for external URLs and files.
1168 // For records we might need to extend this - for instance block display if record is access restricted.
1169 if ($row['freeIndexUid']) {
1170 return true;
1171 }
1172 // Evaluate regularly indexed pages based on item_type:
1173 // External media:
1174 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('index_grlist');
1175 if ($row['item_type']) {
1176 // For external media we will check the access of the parent page on which the media was linked from.
1177 // "phash_t3" is the phash of the parent TYPO3 page row which initiated the indexing of the documents
1178 // in this section. So, selecting for the grlist records belonging to the parent phash-row where the
1179 // current users gr_list exists will help us to know. If this is NOT found, there is still a theoretical
1180 // possibility that another user accessible page would display a link, so maybe the resume of such a
1181 // document here may be unjustified hidden. But better safe than sorry.
1182 if (!$this->isTableUsed('index_grlist')) {
1183 return false;
1184 }
1185
1186 return (bool)$connection->count(
1187 'phash',
1188 'index_grlist',
1189 [
1190 'phash' => (int)$row['phash_t3'],
1191 'gr_list' => $this->frontendUserGroupList
1192 ]
1193 );
1194 } else {
1195 // Ordinary TYPO3 pages:
1196 if ((string)$row['gr_list'] !== (string)$this->frontendUserGroupList) {
1197 // Selecting for the grlist records belonging to the phash-row where the current users gr_list exists.
1198 // If it is found it is proof that this user has direct access to the phash-rows content although
1199 // he did not himself initiate the indexing...
1200 if (!$this->isTableUsed('index_grlist')) {
1201 return false;
1202 }
1203
1204 return (bool)$connection->count(
1205 'phash',
1206 'index_grlist',
1207 [
1208 'phash' => (int)$row['phash'],
1209 'gr_list' => $this->frontendUserGroupList
1210 ]
1211 );
1212 } else {
1213 return true;
1214 }
1215 }
1216 }
1217
1218 /**
1219 * Returns "DESC" or "" depending on the settings of the incoming
1220 * highest/lowest result order (piVars['desc'])
1221 *
1222 * @param bool $inverse If TRUE, inverse the order which is defined by piVars['desc']
1223 * @return string " DESC" or
1224 * @formallyknownas tx_indexedsearch_pi->isDescending
1225 */
1226 protected function getDescendingSortOrderFlag($inverse = false)
1227 {
1228 $desc = $this->descendingSortOrderFlag;
1229 if ($inverse) {
1230 $desc = !$desc;
1231 }
1232 return !$desc ? ' DESC' : '';
1233 }
1234
1235 /**
1236 * Returns a part of a WHERE clause which will filter out records with start/end times or hidden/fe_groups fields
1237 * set to values that should de-select them according to the current time, preview settings or user login.
1238 * Definitely a frontend function.
1239 * THIS IS A VERY IMPORTANT FUNCTION: Basically you must add the output from this function for EVERY select query you create
1240 * for selecting records of tables in your own applications - thus they will always be filtered according to the "enablefields"
1241 * configured in TCA
1242 * Simply calls \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() BUT will send the show_hidden flag along!
1243 * This means this function will work in conjunction with the preview facilities of the frontend engine/Admin Panel.
1244 *
1245 * @param string $table The table for which to get the where clause
1246 * @return string The part of the where clause on the form " AND [fieldname]=0 AND ...". Eg. " AND hidden=0 AND starttime < 123345567
1247 * @see \TYPO3\CMS\Frontend\Page\PageRepository::enableFields()
1248 */
1249 protected function enableFields($table)
1250 {
1251 return $this->getTypoScriptFrontendController()->sys_page->enableFields($table, $table === 'pages' ? $this->getTypoScriptFrontendController()->showHiddenPage : $this->getTypoScriptFrontendController()->showHiddenRecords);
1252 }
1253
1254 /**
1255 * Returns if an item type is a multipage item type
1256 *
1257 * @param string $itemType Item type
1258 * @return bool TRUE if multipage capable
1259 */
1260 protected function multiplePagesType($itemType)
1261 {
1262 /** @var \TYPO3\CMS\IndexedSearch\FileContentParser $fileContentParser */
1263 $fileContentParser = $this->externalParsers[$itemType];
1264 return is_object($fileContentParser) && $fileContentParser->isMultiplePageExtension($itemType);
1265 }
1266
1267 /**
1268 * md5 integer hash
1269 * Using 7 instead of 8 just because that makes the integers lower than
1270 * 32 bit (28 bit) and so they do not interfere with UNSIGNED integers
1271 * or PHP-versions which has varying output from the hexdec function.
1272 *
1273 * @param string $str String to hash
1274 * @return int Integer interpretation of the md5 hash of input string.
1275 */
1276 protected function md5inthash($str)
1277 {
1278 return Utility\IndexedSearchUtility::md5inthash($str);
1279 }
1280
1281 /**
1282 * Check if the tables provided are configured for usage.
1283 * This becomes necessary for extensions that provide additional database
1284 * functionality like indexed_search_mysql.
1285 *
1286 * @param string $table_list Comma-separated list of tables
1287 * @return bool TRUE if given tables are enabled
1288 */
1289 protected function isTableUsed($table_list)
1290 {
1291 return Utility\IndexedSearchUtility::isTableUsed($table_list);
1292 }
1293
1294 /**
1295 * Returns an object reference to the hook object if any
1296 *
1297 * @param string $functionName Name of the function you want to call / hook key
1298 * @return object|NULL Hook object, if any. Otherwise NULL.
1299 */
1300 public function hookRequest($functionName)
1301 {
1302 // Hook: menuConfig_preProcessModMenu
1303 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
1304 $hookObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
1305 if (method_exists($hookObj, $functionName)) {
1306 $hookObj->pObj = $this;
1307 return $hookObj;
1308 }
1309 }
1310 return null;
1311 }
1312
1313 /**
1314 * Search type
1315 * e.g. sentence (20), any part of the word (1)
1316 *
1317 * @return int
1318 */
1319 public function getSearchType()
1320 {
1321 return (int)$this->searchType;
1322 }
1323
1324 /**
1325 * A list of integer which should be root-pages to search from
1326 *
1327 * @return int[]
1328 */
1329 public function getSearchRootPageIdList()
1330 {
1331 return GeneralUtility::intExplode(',', $this->searchRootPageIdList);
1332 }
1333
1334 /**
1335 * Getter for joinPagesForQuery flag
1336 * enabled through TypoScript 'settings.skipExtendToSubpagesChecking'
1337 *
1338 * @return bool
1339 */
1340 public function getJoinPagesForQuery()
1341 {
1342 return $this->joinPagesForQuery;
1343 }
1344
1345 /**
1346 * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
1347 */
1348 protected function getTypoScriptFrontendController()
1349 {
1350 return $GLOBALS['TSFE'];
1351 }
1352
1353 /**
1354 * @return TimeTracker
1355 */
1356 protected function getTimeTracker()
1357 {
1358 return GeneralUtility::makeInstance(TimeTracker::class);
1359 }
1360 }