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