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