[!!!][TASK] Make TimeTracker a singleton
[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 TYPO3\CMS\Core\TimeTracker\TimeTracker;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19 use TYPO3\CMS\Core\Utility\MathUtility;
20 use TYPO3\CMS\IndexedSearch\Indexer;
21 use TYPO3\CMS\IndexedSearch\Utility;
22
23 /**
24 * Index search abstraction to search through the index
25 */
26 class IndexSearchRepository
27 {
28 /**
29 * Indexer object
30 *
31 * @var Indexer
32 */
33 protected $indexerObj;
34
35 /**
36 * External Parsers
37 *
38 * @var array
39 */
40 protected $externalParsers = array();
41
42 /**
43 * Frontend User Group List
44 *
45 * @var string
46 */
47 protected $frontendUserGroupList = '';
48
49 /**
50 * Sections
51 * formally known as $this->piVars['sections']
52 *
53 * @var string
54 */
55 protected $sections = null;
56
57 /**
58 * Search type
59 * formally known as $this->piVars['type']
60 *
61 * @var string
62 */
63 protected $searchType = null;
64
65 /**
66 * Language uid
67 * formally known as $this->piVars['lang']
68 *
69 * @var int
70 */
71 protected $languageUid = null;
72
73 /**
74 * Media type
75 * formally known as $this->piVars['media']
76 *
77 * @var int
78 */
79 protected $mediaType = null;
80
81 /**
82 * Sort order
83 * formally known as $this->piVars['sort_order']
84 *
85 * @var string
86 */
87 protected $sortOrder = null;
88
89 /**
90 * Descending sort order flag
91 * formally known as $this->piVars['desc']
92 *
93 * @var bool
94 */
95 protected $descendingSortOrderFlag = null;
96
97 /**
98 * Result page pointer
99 * formally known as $this->piVars['pointer']
100 *
101 * @var int
102 */
103 protected $resultpagePointer = 0;
104
105 /**
106 * Number of results
107 * formally known as $this->piVars['result']
108 *
109 * @var int
110 */
111 protected $numberOfResults = 10;
112
113 /**
114 * list of all root pages that will be used
115 * If this value is set to less than zero (eg. -1) searching will happen
116 * in ALL of the page tree with no regard to branches at all.
117 *
118 * @var string
119 */
120 protected $searchRootPageIdList;
121
122 /**
123 * formally known as $conf['search.']['searchSkipExtendToSubpagesChecking']
124 * enabled through settings.searchSkipExtendToSubpagesChecking
125 *
126 * @var bool
127 */
128 protected $joinPagesForQuery = false;
129
130 /**
131 * Select clauses for individual words, will be filled during the search
132 *
133 * @var array
134 */
135 protected $wSelClauses = array();
136
137 /**
138 * Flag for exact search count
139 * formally known as $conf['search.']['exactCount']
140 *
141 * Continue counting and checking of results even if we are sure
142 * they are not displayed in this request. This will slow down your
143 * page rendering, but it allows precise search result counters.
144 * enabled through settings.exactCount
145 *
146 * @var bool
147 */
148 protected $useExactCount = false;
149
150 /**
151 * Display forbidden records
152 * formally known as $this->conf['show.']['forbiddenRecords']
153 *
154 * enabled through settings.displayForbiddenRecords
155 *
156 * @var bool
157 */
158 protected $displayForbiddenRecords = false;
159
160 /**
161 * initialize all options that are necessary for the search
162 *
163 * @param array $settings the extbase plugin settings
164 * @param array $searchData the search data
165 * @param array $externalParsers
166 * @param string $searchRootPageIdList
167 * @return void
168 */
169 public function initialize($settings, $searchData, $externalParsers, $searchRootPageIdList)
170 {
171 // Initialize the indexer-class - just to use a few function (for making hashes)
172 $this->indexerObj = GeneralUtility::makeInstance(Indexer::class);
173 $this->externalParsers = $externalParsers;
174 $this->searchRootPageIdList = $searchRootPageIdList;
175 $this->frontendUserGroupList = $this->getTypoScriptFrontendController()->gr_list;
176 // Should we use joinPagesForQuery instead of long lists of uids?
177 if ($settings['searchSkipExtendToSubpagesChecking']) {
178 $this->joinPagesForQuery = 1;
179 }
180 if ($settings['exactCount']) {
181 $this->useExactCount = true;
182 }
183 if ($settings['displayForbiddenRecords']) {
184 $this->displayForbiddenRecords = true;
185 }
186 $this->sections = $searchData['sections'];
187 $this->searchType = $searchData['searchType'];
188 $this->languageUid = $searchData['languageUid'];
189 $this->mediaType = isset($searchData['mediaType']) ? $searchData['mediaType'] : false;
190 $this->sortOrder = $searchData['sortOrder'];
191 $this->descendingSortOrderFlag = $searchData['desc'];
192 $this->resultpagePointer = $searchData['pointer'];
193 if (isset($searchData['numberOfResults']) && is_numeric($searchData['numberOfResults'])) {
194 $this->numberOfResults = (int)$searchData['numberOfResults'];
195 }
196 }
197
198 /**
199 * Get search result rows / data from database. Returned as data in array.
200 *
201 * @param array $searchWords Search word array
202 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
203 * @return bool|array FALSE if no result, otherwise an array with keys for first row, result rows and total number of results found.
204 */
205 public function doSearch($searchWords, $freeIndexUid = -1)
206 {
207 // Getting SQL result pointer:
208 $this->getTimeTracker()->push('Searching result');
209 if ($hookObj = &$this->hookRequest('getResultRows_SQLpointer')) {
210 $res = $hookObj->getResultRows_SQLpointer($searchWords, $freeIndexUid);
211 } else {
212 $res = $this->getResultRows_SQLpointer($searchWords, $freeIndexUid);
213 }
214 $this->getTimeTracker()->pull();
215 // Organize and process result:
216 if ($res) {
217 // Total search-result count
218 $count = $this->getDatabaseConnection()->sql_num_rows($res);
219 // The pointer is set to the result page that is currently being viewed
220 $pointer = MathUtility::forceIntegerInRange($this->resultpagePointer, 0, floor($count / $this->numberOfResults));
221 // Initialize result accumulation variables:
222 $c = 0;
223 // Result pointer: Counts up the position in the current search-result
224 $grouping_phashes = array();
225 // Used to filter out duplicates.
226 $grouping_chashes = array();
227 // Used to filter out duplicates BASED ON cHash.
228 $firstRow = array();
229 // Will hold the first row in result - used to calculate relative hit-ratings.
230 $resultRows = array();
231 // Will hold the results rows for display.
232 // Now, traverse result and put the rows to be displayed into an array
233 // Each row should contain the fields from 'ISEC.*, IP.*' combined
234 // + artificial fields "show_resume" (bool) and "result_number" (counter)
235 while ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) {
236 // Set first row
237 if (!$c) {
238 $firstRow = $row;
239 }
240 // Tells whether we can link directly to a document
241 // or not (depends on possible right problems)
242 $row['show_resume'] = $this->checkResume($row);
243 $phashGr = !in_array($row['phash_grouping'], $grouping_phashes);
244 $chashGr = !in_array(($row['contentHash'] . '.' . $row['data_page_id']), $grouping_chashes);
245 if ($phashGr && $chashGr) {
246 // Only if the resume may be shown are we going to filter out duplicates...
247 if ($row['show_resume'] || $this->displayForbiddenRecords) {
248 // Only on documents which are not multiple pages documents
249 if (!$this->multiplePagesType($row['item_type'])) {
250 $grouping_phashes[] = $row['phash_grouping'];
251 }
252 $grouping_chashes[] = $row['contentHash'] . '.' . $row['data_page_id'];
253 // Increase the result pointer
254 $c++;
255 // All rows for display is put into resultRows[]
256 if ($c > $pointer * $this->numberOfResults && $c <= $pointer * $this->numberOfResults + $this->numberOfResults) {
257 $row['result_number'] = $c;
258 $resultRows[] = $row;
259 // This may lead to a problem: If the result check is not stopped here, the search will take longer.
260 // However the result counter will not filter out grouped cHashes/pHashes that were not processed yet.
261 // You can change this behavior using the "search.exactCount" property (see above).
262 if (!$this->useExactCount && $c + 1 > ($pointer + 1) * $this->numberOfResults) {
263 break;
264 }
265 }
266 } else {
267 // Skip this row if the user cannot
268 // view it (missing permission)
269 $count--;
270 }
271 } else {
272 // For each time a phash_grouping document is found
273 // (which is thus not displayed) the search-result count is reduced,
274 // so that it matches the number of rows displayed.
275 $count--;
276 }
277 }
278
279 $this->getDatabaseConnection()->sql_free_result($res);
280
281 return array(
282 'resultRows' => $resultRows,
283 'firstRow' => $firstRow,
284 'count' => $count
285 );
286 } else {
287 // No results found
288 return false;
289 }
290 }
291
292 /**
293 * Gets a SQL result pointer to traverse for the search records.
294 *
295 * @param array $searchWords Search words
296 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
297 * @return bool|\mysqli_result
298 */
299 protected function getResultRows_SQLpointer($searchWords, $freeIndexUid = -1)
300 {
301 // This SEARCHES for the searchwords in $searchWords AND returns a
302 // COMPLETE list of phash-integers of the matches.
303 $list = $this->getPhashList($searchWords);
304 // Perform SQL Search / collection of result rows array:
305 if ($list) {
306 // Do the search:
307 $this->getTimeTracker()->push('execFinalQuery');
308 $res = $this->execFinalQuery($list, $freeIndexUid);
309 $this->getTimeTracker()->pull();
310 return $res;
311 } else {
312 return false;
313 }
314 }
315
316 /***********************************
317 *
318 * Helper functions on searching (SQL)
319 *
320 ***********************************/
321 /**
322 * Returns a COMPLETE list of phash-integers matching the search-result composed of the search-words in the $searchWords array.
323 * The list of phash integers are unsorted and should be used for subsequent selection of index_phash records for display of the result.
324 *
325 * @param array $searchWords Search word array
326 * @return string List of integers
327 */
328 protected function getPhashList($searchWords)
329 {
330 // Initialize variables:
331 $c = 0;
332 // This array accumulates the phash-values
333 $totalHashList = array();
334 $this->wSelClauses = array();
335 // Traverse searchwords; for each, select all phash integers and merge/diff/intersect them with previous word (based on operator)
336 foreach ($searchWords as $k => $v) {
337 // Making the query for a single search word based on the search-type
338 $sWord = $v['sword'];
339 $theType = (string)$this->searchType;
340 // If there are spaces in the search-word, make a full text search instead.
341 if (strstr($sWord, ' ')) {
342 $theType = 20;
343 }
344 $this->getTimeTracker()->push('SearchWord "' . $sWord . '" - $theType=' . $theType);
345 // Perform search for word:
346 switch ($theType) {
347 case '1':
348 // Part of word
349 $res = $this->searchWord($sWord, Utility\LikeWildcard::BOTH);
350 break;
351 case '2':
352 // First part of word
353 $res = $this->searchWord($sWord, Utility\LikeWildcard::RIGHT);
354 break;
355 case '3':
356 // Last part of word
357 $res = $this->searchWord($sWord, Utility\LikeWildcard::LEFT);
358 break;
359 case '10':
360 // Sounds like
361 /**
362 * Indexer object
363 *
364 * @var Indexer
365 */
366 $indexerObj = GeneralUtility::makeInstance(Indexer::class);
367 // Perform metaphone search
368 $storeMetaphoneInfoAsWords = !$this->isTableUsed('index_words');
369 $res = $this->searchMetaphone($indexerObj->metaphone($sWord, $storeMetaphoneInfoAsWords));
370 unset($indexerObj);
371 break;
372 case '20':
373 // Sentence
374 $res = $this->searchSentence($sWord);
375 // If there is a fulltext search for a sentence there is
376 // a likeliness that sorting cannot be done by the rankings
377 // from the rel-table (because no relations will exist for the
378 // sentence in the word-table). So therefore mtime is used instead.
379 // It is not required, but otherwise some hits may be left out.
380 $this->sortOrder = 'mtime';
381 break;
382 default:
383 // Distinct word
384 $res = $this->searchDistinct($sWord);
385 }
386 // If there was a query to do, then select all phash-integers which resulted from this.
387 if ($res) {
388 // Get phash list by searching for it:
389 $phashList = array();
390 while ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) {
391 $phashList[] = $row['phash'];
392 }
393 $this->getDatabaseConnection()->sql_free_result($res);
394 // Here the phash list are merged with the existing result based on whether we are dealing with OR, NOT or AND operations.
395 if ($c) {
396 switch ($v['oper']) {
397 case 'OR':
398 $totalHashList = array_unique(array_merge($phashList, $totalHashList));
399 break;
400 case 'AND NOT':
401 $totalHashList = array_diff($totalHashList, $phashList);
402 break;
403 default:
404 // AND...
405 $totalHashList = array_intersect($totalHashList, $phashList);
406 }
407 } else {
408 // First search
409 $totalHashList = $phashList;
410 }
411 }
412 $this->getTimeTracker()->pull();
413 $c++;
414 }
415 return implode(',', $totalHashList);
416 }
417
418 /**
419 * Returns a query which selects the search-word from the word/rel tables.
420 *
421 * @param string $wordSel WHERE clause selecting the word from phash
422 * @param string $additionalWhereClause Additional AND clause in the end of the query.
423 * @return bool|\mysqli_result SQL result pointer
424 */
425 protected function execPHashListQuery($wordSel, $additionalWhereClause = '')
426 {
427 return $this->getDatabaseConnection()->exec_SELECTquery(
428 'IR.phash',
429 'index_words IW, index_rel IR, index_section ISEC',
430 $wordSel . ' AND IW.wid=IR.wid AND ISEC.phash=IR.phash' . $this->sectionTableWhere() . $additionalWhereClause,
431 'IR.phash'
432 );
433 }
434
435 /**
436 * Search for a word
437 *
438 * @param string $sWord the search word
439 * @param int $wildcard Bit-field of Utility\LikeWildcard
440 * @return bool|\mysqli_result SQL result pointer
441 */
442 protected function searchWord($sWord, $wildcard)
443 {
444 $likeWildcard = Utility\LikeWildcard::cast($wildcard);
445 $wSel = $likeWildcard->getLikeQueryPart(
446 'index_words',
447 'IW.baseword',
448 $sWord
449 );
450 $this->wSelClauses[] = $wSel;
451 return $this->execPHashListQuery($wSel, ' AND is_stopword=0');
452 }
453
454 /**
455 * Search for one distinct word
456 *
457 * @param string $sWord the search word
458 * @return bool|\mysqli_result SQL result pointer
459 */
460 protected function searchDistinct($sWord)
461 {
462 $wSel = 'IW.wid=' . $this->md5inthash($sWord);
463 $this->wSelClauses[] = $wSel;
464 return $this->execPHashListQuery($wSel, ' AND is_stopword=0');
465 }
466
467 /**
468 * Search for a sentence
469 *
470 * @param string $sWord the search word
471 * @return bool|\mysqli_result SQL result pointer
472 */
473 protected function searchSentence($sWord)
474 {
475 $this->wSelClauses[] = '1=1';
476 $likeWildcard = Utility\LikeWildcard::cast(Utility\LikeWildcard::BOTH);
477 $likePart = $likeWildcard->getLikeQueryPart(
478 'index_fulltext',
479 'IFT.fulltextdata',
480 $sWord
481 );
482
483 return $this->getDatabaseConnection()->exec_SELECTquery(
484 'ISEC.phash',
485 'index_section ISEC, index_fulltext IFT',
486 $likePart . ' AND ISEC.phash = IFT.phash' . $this->sectionTableWhere(),
487 'ISEC.phash'
488 );
489 }
490
491 /**
492 * Search for a metaphone word
493 *
494 * @param string $sWord the search word
495 * @return bool|\mysqli_result SQL result pointer
496 */
497 protected function searchMetaphone($sWord)
498 {
499 $wSel = 'IW.metaphone=' . $sWord;
500 $this->wSelClauses[] = $wSel;
501 return $this->execPHashListQuery($wSel, ' AND is_stopword=0');
502 }
503
504 /**
505 * Returns AND statement for selection of section in database. (rootlevel 0-2 + page_id)
506 *
507 * @return string AND clause for selection of section in database.
508 */
509 public function sectionTableWhere()
510 {
511 $whereClause = '';
512 $match = false;
513 if (!($this->searchRootPageIdList < 0)) {
514 $whereClause = ' AND ISEC.rl0 IN (' . $this->searchRootPageIdList . ') ';
515 }
516 if (substr($this->sections, 0, 4) == 'rl1_') {
517 $list = implode(',', GeneralUtility::intExplode(',', substr($this->sections, 4)));
518 $whereClause .= ' AND ISEC.rl1 IN (' . $list . ')';
519 $match = true;
520 } elseif (substr($this->sections, 0, 4) == 'rl2_') {
521 $list = implode(',', GeneralUtility::intExplode(',', substr($this->sections, 4)));
522 $whereClause .= ' AND ISEC.rl2 IN (' . $list . ')';
523 $match = true;
524 } elseif (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'])) {
525 // Traversing user configured fields to see if any of those are used to limit search to a section:
526 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'] as $fieldName => $rootLineLevel) {
527 if (substr($this->sections, 0, strlen($fieldName) + 1) == $fieldName . '_') {
528 $list = implode(',', GeneralUtility::intExplode(',', substr($this->sections, strlen($fieldName) + 1)));
529 $whereClause .= ' AND ISEC.' . $fieldName . ' IN (' . $list . ')';
530 $match = true;
531 break;
532 }
533 }
534 }
535 // If no match above, test the static types:
536 if (!$match) {
537 switch ((string)$this->sections) {
538 case '-1':
539 $whereClause .= ' AND ISEC.page_id=' . $this->getTypoScriptFrontendController()->id;
540 break;
541 case '-2':
542 $whereClause .= ' AND ISEC.rl2=0';
543 break;
544 case '-3':
545 $whereClause .= ' AND ISEC.rl2>0';
546 break;
547 }
548 }
549 return $whereClause;
550 }
551
552 /**
553 * Returns AND statement for selection of media type
554 *
555 * @return string AND statement for selection of media type
556 */
557 public function mediaTypeWhere()
558 {
559 switch ($this->mediaType) {
560 case '0':
561 // '0' => 'Kun TYPO3 sider',
562 $whereClause = ' AND IP.item_type=' . $this->getDatabaseConnection()->fullQuoteStr('0', 'index_phash');
563 break;
564 case '-2':
565 // All external documents
566 $whereClause = ' AND IP.item_type!=' . $this->getDatabaseConnection()->fullQuoteStr('0', 'index_phash');
567 break;
568 case false:
569
570 case '-1':
571 // All content
572 $whereClause = '';
573 break;
574 default:
575 $whereClause = ' AND IP.item_type=' . $this->getDatabaseConnection()->fullQuoteStr($this->mediaType, 'index_phash');
576 }
577 return $whereClause;
578 }
579
580 /**
581 * Returns AND statement for selection of language
582 *
583 * @return string AND statement for selection of language
584 */
585 public function languageWhere()
586 {
587 // -1 is the same as ALL language.
588 if ($this->languageUid >= 0) {
589 return ' AND IP.sys_language_uid=' . (int)$this->languageUid;
590 }
591 return '';
592 }
593
594 /**
595 * Where-clause for free index-uid value.
596 *
597 * @param int $freeIndexUid Free Index UID value to limit search to.
598 * @return string WHERE SQL clause part.
599 */
600 public function freeIndexUidWhere($freeIndexUid)
601 {
602 $freeIndexUid = (int)$freeIndexUid;
603 if ($freeIndexUid >= 0) {
604 // First, look if the freeIndexUid is a meta configuration:
605 $indexCfgRec = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('indexcfgs', 'index_config', 'type=5 AND uid=' . $freeIndexUid . $this->enableFields('index_config'));
606 if (is_array($indexCfgRec)) {
607 $refs = GeneralUtility::trimExplode(',', $indexCfgRec['indexcfgs']);
608 // Default value to protect against empty array.
609 $list = array(-99);
610 foreach ($refs as $ref) {
611 list($table, $uid) = GeneralUtility::revExplode('_', $ref, 2);
612 $uid = (int)$uid;
613 switch ($table) {
614 case 'index_config':
615 $idxRec = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('uid', 'index_config', 'uid=' . $uid . $this->enableFields('index_config'));
616 if ($idxRec) {
617 $list[] = $uid;
618 }
619 break;
620 case 'pages':
621 $indexCfgRecordsFromPid = $this->getDatabaseConnection()->exec_SELECTgetRows('uid', 'index_config', 'pid=' . $uid . $this->enableFields('index_config'));
622 foreach ($indexCfgRecordsFromPid as $idxRec) {
623 $list[] = $idxRec['uid'];
624 }
625 break;
626 }
627 }
628 $list = array_unique($list);
629 } else {
630 $list = array($freeIndexUid);
631 }
632 return ' AND IP.freeIndexUid IN (' . implode(',', $list) . ')';
633 }
634 return '';
635 }
636
637 /**
638 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
639 *
640 * @param string $list List of phash integers which match the search.
641 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
642 * @return bool|\mysqli_result Query result pointer
643 */
644 protected function execFinalQuery($list, $freeIndexUid = -1)
645 {
646 // Setting up methods of filtering results
647 // based on page types, access, etc.
648 $page_join = '';
649 // Indexing configuration clause:
650 $freeIndexUidClause = $this->freeIndexUidWhere($freeIndexUid);
651 // Calling hook for alternative creation of page ID list
652 if ($hookObj = $this->hookRequest('execFinalQuery_idList')) {
653 $page_where = $hookObj->execFinalQuery_idList($list);
654 } elseif ($this->joinPagesForQuery) {
655 // Alternative to getting all page ids by ->getTreeList() where
656 // "excludeSubpages" is NOT respected.
657 $page_join = ',
658 pages';
659 $page_where = 'pages.uid = ISEC.page_id
660 ' . $this->enableFields('pages') . '
661 AND pages.no_search=0
662 AND pages.doktype<200
663 ';
664 } elseif ($this->searchRootPageIdList >= 0) {
665 // Collecting all pages IDs in which to search;
666 // filtering out ALL pages that are not accessible due to enableFields.
667 // Does NOT look for "no_search" field!
668 $siteIdNumbers = GeneralUtility::intExplode(',', $this->searchRootPageIdList);
669 $pageIdList = array();
670 foreach ($siteIdNumbers as $rootId) {
671 $pageIdList[] = $this->getTypoScriptFrontendController()->cObj->getTreeList(-1 * $rootId, 9999);
672 }
673 $page_where = 'ISEC.page_id IN (' . implode(',', $pageIdList) . ')';
674 } else {
675 // Disable everything... (select all)
676 $page_where = '1=1';
677 }
678 // otherwise select all / disable everything
679 // If any of the ranking sortings are selected, we must make a
680 // join with the word/rel-table again, because we need to
681 // calculate ranking based on all search-words found.
682 if (substr($this->sortOrder, 0, 5) === 'rank_') {
683 switch ($this->sortOrder) {
684 case 'rank_flag':
685 // This gives priority to word-position (max-value) so that words in title, keywords, description counts more than in content.
686 // The ordering is refined with the frequency sum as well.
687 $grsel = 'MAX(IR.flags) AS order_val1, SUM(IR.freq) AS order_val2';
688 $orderBy = 'order_val1' . $this->getDescendingSortOrderFlag() . ', order_val2' . $this->getDescendingSortOrderFlag();
689 break;
690 case 'rank_first':
691 // Results in average position of search words on page.
692 // Must be inversely sorted (low numbers are closer to top)
693 $grsel = 'AVG(IR.first) AS order_val';
694 $orderBy = 'order_val' . $this->getDescendingSortOrderFlag(true);
695 break;
696 case 'rank_count':
697 // Number of words found
698 $grsel = 'SUM(IR.count) AS order_val';
699 $orderBy = 'order_val' . $this->getDescendingSortOrderFlag();
700 break;
701 default:
702 // Frequency sum. I'm not sure if this is the best way to do
703 // it (make a sum...). Or should it be the average?
704 $grsel = 'SUM(IR.freq) AS order_val';
705 $orderBy = 'order_val' . $this->getDescendingSortOrderFlag();
706 }
707 $wordSel = '';
708 if (!empty($this->wSelClauses)) {
709 // So, words are imploded into an OR statement (no "sentence search" should be done here - may deselect results)
710 $wordSel = '(' . implode(' OR ', $this->wSelClauses) . ') AND ';
711 }
712 $res = $this->getDatabaseConnection()->exec_SELECTquery(
713 'ISEC.*, IP.*, ' . $grsel,
714 'index_words IW,
715 index_rel IR,
716 index_section ISEC,
717 index_phash IP' . $page_join,
718 $wordSel .
719 'IP.phash IN (' . $list . ') ' .
720 $this->mediaTypeWhere() . ' ' . $this->languageWhere() . $freeIndexUidClause . '
721 AND IW.wid=IR.wid
722 AND ISEC.phash = IR.phash
723 AND IP.phash = IR.phash
724 AND ' . $page_where,
725 'IP.phash,ISEC.phash,ISEC.phash_t3,ISEC.rl0,ISEC.rl1,ISEC.rl2 ,ISEC.page_id,ISEC.uniqid,IP.phash_grouping,IP.data_filename ,IP.data_page_id ,IP.data_page_reg1,IP.data_page_type,IP.data_page_mp,IP.gr_list,IP.item_type,IP.item_title,IP.item_description,IP.item_mtime,IP.tstamp,IP.item_size,IP.contentHash,IP.crdate,IP.parsetime,IP.sys_language_uid,IP.item_crdate,IP.cHashParams,IP.externalUrl,IP.recordUid,IP.freeIndexUid,IP.freeIndexSetId',
726 $orderBy
727 );
728 } else {
729 // Otherwise, if sorting are done with the pages table or other fields,
730 // there is no need for joining with the rel/word tables:
731 $orderBy = '';
732 switch ((string)$this->sortOrder) {
733 case 'title':
734 $orderBy = 'IP.item_title' . $this->getDescendingSortOrderFlag();
735 break;
736 case 'crdate':
737 $orderBy = 'IP.item_crdate' . $this->getDescendingSortOrderFlag();
738 break;
739 case 'mtime':
740 $orderBy = 'IP.item_mtime' . $this->getDescendingSortOrderFlag();
741 break;
742 }
743 $res = $this->getDatabaseConnection()->exec_SELECTquery('ISEC.*, IP.*', 'index_phash IP,index_section ISEC' . $page_join, 'IP.phash IN (' . $list . ') ' . $this->mediaTypeWhere() . $this->languageWhere() . $freeIndexUidClause . '
744 AND IP.phash = ISEC.phash AND ' . $page_where, 'IP.phash,ISEC.phash,ISEC.phash_t3,ISEC.rl0,ISEC.rl1,ISEC.rl2 ,ISEC.page_id,ISEC.uniqid,IP.phash_grouping,IP.data_filename ,IP.data_page_id ,IP.data_page_reg1,IP.data_page_type,IP.data_page_mp,IP.gr_list,IP.item_type,IP.item_title,IP.item_description,IP.item_mtime,IP.tstamp,IP.item_size,IP.contentHash,IP.crdate,IP.parsetime,IP.sys_language_uid,IP.item_crdate,IP.cHashParams,IP.externalUrl,IP.recordUid,IP.freeIndexUid,IP.freeIndexSetId', $orderBy);
745 }
746 return $res;
747 }
748
749 /**
750 * Checking if the resume can be shown for the search result
751 * (depending on whether the rights are OK)
752 * ? Should it also check for gr_list "0,-1"?
753 *
754 * @param array $row Result row array.
755 * @return bool Returns TRUE if resume can safely be shown
756 */
757 protected function checkResume($row)
758 {
759 // If the record is indexed by an indexing configuration, just show it.
760 // At least this is needed for external URLs and files.
761 // For records we might need to extend this - for instance block display if record is access restricted.
762 if ($row['freeIndexUid']) {
763 return true;
764 }
765 // Evaluate regularly indexed pages based on item_type:
766 // External media:
767 if ($row['item_type']) {
768 // For external media we will check the access of the parent page on which the media was linked from.
769 // "phash_t3" is the phash of the parent TYPO3 page row which initiated the indexing of the documents in this section.
770 // So, selecting for the grlist records belonging to the parent phash-row where the current users gr_list exists will help us to know.
771 // If this is NOT found, there is still a theoretical possibility that another user accessible page would display a link, so maybe the resume of such a document here may be unjustified hidden. But better safe than sorry.
772 if ($this->isTableUsed('index_grlist')) {
773 $res = $this->getDatabaseConnection()->exec_SELECTquery('phash', 'index_grlist', 'phash=' . (int)$row['phash_t3'] . ' AND gr_list=' . $this->getDatabaseConnection()->fullQuoteStr($this->frontendUserGroupList, 'index_grlist'));
774 } else {
775 $res = false;
776 }
777 if ($res && $this->getDatabaseConnection()->sql_num_rows($res)) {
778 return true;
779 } else {
780 return false;
781 }
782 } else {
783 // Ordinary TYPO3 pages:
784 if ((string)$row['gr_list'] !== (string)$this->frontendUserGroupList) {
785 // Selecting for the grlist records belonging to the phash-row where the current users gr_list exists. If it is found it is proof that this user has direct access to the phash-rows content although he did not himself initiate the indexing...
786 if ($this->isTableUsed('index_grlist')) {
787 $res = $this->getDatabaseConnection()->exec_SELECTquery('phash', 'index_grlist', 'phash=' . (int)$row['phash'] . ' AND gr_list=' . $this->getDatabaseConnection()->fullQuoteStr($this->frontendUserGroupList, 'index_grlist'));
788 } else {
789 $res = false;
790 }
791 if ($res && $this->getDatabaseConnection()->sql_num_rows($res)) {
792 return true;
793 } else {
794 return false;
795 }
796 } else {
797 return true;
798 }
799 }
800 }
801
802 /**
803 * Returns "DESC" or "" depending on the settings of the incoming
804 * highest/lowest result order (piVars['desc'])
805 *
806 * @param bool $inverse If TRUE, inverse the order which is defined by piVars['desc']
807 * @return string " DESC" or
808 * @formallyknownas tx_indexedsearch_pi->isDescending
809 */
810 protected function getDescendingSortOrderFlag($inverse = false)
811 {
812 $desc = $this->descendingSortOrderFlag;
813 if ($inverse) {
814 $desc = !$desc;
815 }
816 return !$desc ? ' DESC' : '';
817 }
818
819 /**
820 * Returns a part of a WHERE clause which will filter out records with start/end times or hidden/fe_groups fields
821 * set to values that should de-select them according to the current time, preview settings or user login.
822 * Definitely a frontend function.
823 * THIS IS A VERY IMPORTANT FUNCTION: Basically you must add the output from this function for EVERY select query you create
824 * for selecting records of tables in your own applications - thus they will always be filtered according to the "enablefields"
825 * configured in TCA
826 * Simply calls \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() BUT will send the show_hidden flag along!
827 * This means this function will work in conjunction with the preview facilities of the frontend engine/Admin Panel.
828 *
829 * @param string $table The table for which to get the where clause
830 * @return string The part of the where clause on the form " AND [fieldname]=0 AND ...". Eg. " AND hidden=0 AND starttime < 123345567
831 * @see \TYPO3\CMS\Frontend\Page\PageRepository::enableFields()
832 */
833 protected function enableFields($table)
834 {
835 return $this->getTypoScriptFrontendController()->sys_page->enableFields($table, $table === 'pages' ? $this->getTypoScriptFrontendController()->showHiddenPage : $this->getTypoScriptFrontendController()->showHiddenRecords);
836 }
837
838 /**
839 * Returns if an item type is a multipage item type
840 *
841 * @param string $itemType Item type
842 * @return bool TRUE if multipage capable
843 */
844 protected function multiplePagesType($itemType)
845 {
846 /** @var \TYPO3\CMS\IndexedSearch\FileContentParser $fileContentParser */
847 $fileContentParser = $this->externalParsers[$itemType];
848 return is_object($fileContentParser) && $fileContentParser->isMultiplePageExtension($itemType);
849 }
850
851 /**
852 * md5 integer hash
853 * Using 7 instead of 8 just because that makes the integers lower than
854 * 32 bit (28 bit) and so they do not interfere with UNSIGNED integers
855 * or PHP-versions which has varying output from the hexdec function.
856 *
857 * @param string $str String to hash
858 * @return int Integer intepretation of the md5 hash of input string.
859 */
860 protected function md5inthash($str)
861 {
862 return Utility\IndexedSearchUtility::md5inthash($str);
863 }
864
865 /**
866 * Check if the tables provided are configured for usage.
867 * This becomes necessary for extensions that provide additional database
868 * functionality like indexed_search_mysql.
869 *
870 * @param string $table_list Comma-separated list of tables
871 * @return bool TRUE if given tables are enabled
872 */
873 protected function isTableUsed($table_list)
874 {
875 return Utility\IndexedSearchUtility::isTableUsed($table_list);
876 }
877
878 /**
879 * Returns an object reference to the hook object if any
880 *
881 * @param string $functionName Name of the function you want to call / hook key
882 * @return object|NULL Hook object, if any. Otherwise NULL.
883 */
884 public function hookRequest($functionName)
885 {
886 // Hook: menuConfig_preProcessModMenu
887 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
888 $hookObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
889 if (method_exists($hookObj, $functionName)) {
890 $hookObj->pObj = $this;
891 return $hookObj;
892 }
893 }
894 return null;
895 }
896
897 /**
898 * Search type
899 * e.g. sentence (20), any part of the word (1)
900 *
901 * @return int
902 */
903 public function getSearchType()
904 {
905 return (int)$this->searchType;
906 }
907
908 /**
909 * A list of integer which should be root-pages to search from
910 *
911 * @return int[]
912 */
913 public function getSearchRootPageIdList()
914 {
915 return GeneralUtility::intExplode(',', $this->searchRootPageIdList);
916 }
917
918 /**
919 * Getter for joinPagesForQuery flag
920 * enabled through TypoScript 'settings.skipExtendToSubpagesChecking'
921 *
922 * @return bool
923 */
924 public function getJoinPagesForQuery()
925 {
926 return $this->joinPagesForQuery;
927 }
928
929 /**
930 * Returns the database connection
931 *
932 * @return \TYPO3\CMS\Core\Database\DatabaseConnection
933 */
934 protected function getDatabaseConnection()
935 {
936 return $GLOBALS['TYPO3_DB'];
937 }
938
939 /**
940 * @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
941 */
942 protected function getTypoScriptFrontendController()
943 {
944 return $GLOBALS['TSFE'];
945 }
946
947 /**
948 * @return TimeTracker
949 */
950 protected function getTimeTracker()
951 {
952 return GeneralUtility::makeInstance(TimeTracker::class);
953 }
954 }