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