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