[!!!][TASK] Make TimeTracker a singleton
[Packages/TYPO3.CMS.git] / typo3 / sysext / indexed_search / Classes / Controller / SearchFormController.php
1 <?php
2 namespace TYPO3\CMS\IndexedSearch\Controller;
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\Charset\CharsetConverter;
18 use TYPO3\CMS\Core\Html\HtmlParser;
19 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Core\Utility\MathUtility;
22 use TYPO3\CMS\IndexedSearch\Utility;
23
24 /**
25 * Index search frontend
26 *
27 * Creates a searchform for indexed search. Indexing must be enabled
28 * for this to make sense.
29 */
30 class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin
31 {
32 /**
33 * Used as fieldname prefix
34 *
35 * @var string
36 */
37 public $prefixId = 'tx_indexedsearch';
38
39 /**
40 * Extension key.
41 *
42 * @var string
43 */
44 public $extKey = 'indexed_search';
45
46 /**
47 * See document for info about this flag...
48 *
49 * @var int
50 */
51 public $join_pages = 0;
52
53 public $defaultResultNumber = 10;
54
55 /**
56 * Internal variable
57 *
58 * @var array
59 */
60 public $operator_translate_table = array(array('+', 'AND'), array('|', 'OR'), array('-', 'AND NOT'));
61
62 /**
63 * Root-page ids to search in (rl0 field where clause, see initialize() function)
64 *
65 * @var int|string id or comma separated list of ids
66 */
67 public $wholeSiteIdList = 0;
68
69 /**
70 * Search Words and operators
71 *
72 * @var array
73 */
74 public $sWArr = array();
75
76 /**
77 * Selector box values for search configuration form
78 *
79 * @var array
80 */
81 public $optValues = array();
82
83 /**
84 * Will hold the first row in result - used to calculate relative hit-ratings.
85 *
86 * @var array
87 */
88 public $firstRow = array();
89
90 /**
91 * Caching of page path
92 *
93 * @var array
94 */
95 public $cache_path = array();
96
97 /**
98 * Caching of root line data
99 *
100 * @var array
101 */
102 public $cache_rl = array();
103
104 /**
105 * Required fe_groups memberships for display of a result.
106 *
107 * @var array
108 */
109 public $fe_groups_required = array();
110
111 /**
112 * sys_domain records
113 *
114 * @var array
115 */
116 public $domain_records = array();
117
118 /**
119 * Select clauses for individual words
120 *
121 * @var array
122 */
123 public $wSelClauses = array();
124
125 /**
126 * Page tree sections for search result.
127 *
128 * @var array
129 */
130 public $resultSections = array();
131
132 /**
133 * External parser objects
134 * @var array
135 */
136 public $external_parsers = array();
137
138 /**
139 * Storage of icons....
140 *
141 * @var array
142 */
143 public $iconFileNameCache = array();
144
145 /**
146 * Will hold the content of $conf['templateFile']
147 *
148 * @var string
149 */
150 public $templateCode = '';
151
152 public $hiddenFieldList = 'ext, type, defOp, media, order, group, lang, desc, results';
153
154 /**
155 * Indexer configuration, coming from $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['indexed_search']
156 *
157 * @var array
158 */
159 public $indexerConfig = array();
160
161 /**
162 * @var bool
163 */
164 public $enableMetaphoneSearch = false;
165
166 public $storeMetaphoneInfoAsWords;
167
168 /**
169 * Lexer object
170 *
171 * @var \TYPO3\CMS\IndexedSearch\Lexer
172 */
173 public $lexerObj;
174
175 /**
176 * @var TimeTracker
177 */
178 protected $timeTracker;
179
180 /**
181 * @var CharsetConverter
182 */
183 protected $charsetConverter;
184
185 /**
186 * Main function, called from TypoScript as a USER_INT object.
187 *
188 * @param string $content Content input, ignore (just put blank string)
189 * @param array $conf TypoScript configuration of the plugin!
190 * @return string HTML code for the search form / result display.
191 */
192 public function main($content, $conf)
193 {
194 // Initialize:
195 $this->charsetConverter = GeneralUtility::makeInstance(CharsetConverter::class);
196 $this->conf = $conf;
197 $this->pi_loadLL('EXT:indexed_search/Resources/Private/Language/locallang_pi.xlf');
198 $this->pi_setPiVarDefaults();
199 // Initialize:
200 $this->initialize();
201 // Do search:
202 // If there were any search words entered...
203 if (is_array($this->sWArr) && !empty($this->sWArr)) {
204 $content = $this->doSearch($this->sWArr);
205 }
206 // Finally compile all the content, form, messages and results:
207 $content = $this->makeSearchForm($this->optValues) . $this->printRules() . $content;
208 return $this->pi_wrapInBaseClass($content);
209 }
210
211 /**
212 * Initialize internal variables, especially selector box values for the search form and search words
213 *
214 * @return void
215 */
216 public function initialize()
217 {
218 // Indexer configuration from Extension Manager interface:
219 $this->indexerConfig = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['indexed_search']);
220 $this->enableMetaphoneSearch = (bool)$this->indexerConfig['enableMetaphoneSearch'];
221 $this->storeMetaphoneInfoAsWords = !\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_words');
222 $this->timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
223 // Initialize external document parsers for icon display and other soft operations
224 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['external_parsers'])) {
225 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['external_parsers'] as $extension => $_objRef) {
226 $this->external_parsers[$extension] = GeneralUtility::getUserObj($_objRef);
227 // Init parser and if it returns FALSE, unset its entry again:
228 if (!$this->external_parsers[$extension]->softInit($extension)) {
229 unset($this->external_parsers[$extension]);
230 }
231 }
232 }
233 // Init lexer (used to post-processing of search words)
234 $lexerObjRef = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['lexer'] ?: \TYPO3\CMS\IndexedSearch\Lexer::class;
235 $this->lexerObj = GeneralUtility::getUserObj($lexerObjRef);
236 // If "_sections" is set, this value overrides any existing value.
237 if ($this->piVars['_sections']) {
238 $this->piVars['sections'] = $this->piVars['_sections'];
239 }
240 // If "_sections" is set, this value overrides any existing value.
241 if ($this->piVars['_freeIndexUid'] !== '_') {
242 $this->piVars['freeIndexUid'] = $this->piVars['_freeIndexUid'];
243 }
244 // Add previous search words to current
245 if ($this->piVars['sword_prev_include'] && $this->piVars['sword_prev']) {
246 $this->piVars['sword'] = trim($this->piVars['sword_prev']) . ' ' . $this->piVars['sword'];
247 }
248 $this->piVars['results'] = MathUtility::forceIntegerInRange($this->piVars['results'], 1, 100000, $this->defaultResultNumber);
249 // Make sure that some cropping and markup constants used later are defined
250 $this->loadSettings();
251
252 // Selector-box values defined here:
253 $this->optValues = array(
254 'type' => array(
255 '0' => $this->pi_getLL('opt_type_0'),
256 '1' => $this->pi_getLL('opt_type_1'),
257 '2' => $this->pi_getLL('opt_type_2'),
258 '3' => $this->pi_getLL('opt_type_3'),
259 '10' => $this->pi_getLL('opt_type_10'),
260 '20' => $this->pi_getLL('opt_type_20')
261 ),
262 'defOp' => array(
263 '0' => $this->pi_getLL('opt_defOp_0'),
264 '1' => $this->pi_getLL('opt_defOp_1')
265 ),
266 'sections' => array(
267 '0' => $this->pi_getLL('opt_sections_0'),
268 '-1' => $this->pi_getLL('opt_sections_-1'),
269 '-2' => $this->pi_getLL('opt_sections_-2'),
270 '-3' => $this->pi_getLL('opt_sections_-3')
271 ),
272 'freeIndexUid' => array(
273 '-1' => $this->pi_getLL('opt_freeIndexUid_-1'),
274 '-2' => $this->pi_getLL('opt_freeIndexUid_-2'),
275 '0' => $this->pi_getLL('opt_freeIndexUid_0')
276 ),
277 'media' => array(
278 '-1' => $this->pi_getLL('opt_media_-1'),
279 '0' => $this->pi_getLL('opt_media_0'),
280 '-2' => $this->pi_getLL('opt_media_-2')
281 ),
282 'order' => array(
283 'rank_flag' => $this->pi_getLL('opt_order_rank_flag'),
284 'rank_freq' => $this->pi_getLL('opt_order_rank_freq'),
285 'rank_first' => $this->pi_getLL('opt_order_rank_first'),
286 'rank_count' => $this->pi_getLL('opt_order_rank_count'),
287 'mtime' => $this->pi_getLL('opt_order_mtime'),
288 'title' => $this->pi_getLL('opt_order_title'),
289 'crdate' => $this->pi_getLL('opt_order_crdate')
290 ),
291 'group' => array(
292 'sections' => $this->pi_getLL('opt_group_sections'),
293 'flat' => $this->pi_getLL('opt_group_flat')
294 ),
295 'lang' => array(
296 -1 => $this->pi_getLL('opt_lang_-1'),
297 0 => $this->pi_getLL('opt_lang_0')
298 ),
299 'desc' => array(
300 '0' => $this->pi_getLL('opt_desc_0'),
301 '1' => $this->pi_getLL('opt_desc_1')
302 ),
303 'results' => array(
304 '10' => '10',
305 '20' => '20',
306 '50' => '50',
307 '100' => '100'
308 )
309 );
310 // Remove this option if metaphone search is disabled)
311 if (!$this->enableMetaphoneSearch) {
312 unset($this->optValues['type']['10']);
313 }
314 // Free Index Uid:
315 if ($this->conf['search.']['defaultFreeIndexUidList']) {
316 $uidList = GeneralUtility::intExplode(',', $this->conf['search.']['defaultFreeIndexUidList']);
317 $indexCfgRecords = $this->databaseConnection->exec_SELECTgetRows('uid,title', 'index_config', 'uid IN (' . implode(',', $uidList) . ')' . $this->cObj->enableFields('index_config'), '', '', '', 'uid');
318 foreach ($uidList as $uidValue) {
319 if (is_array($indexCfgRecords[$uidValue])) {
320 $this->optValues['freeIndexUid'][$uidValue] = $indexCfgRecords[$uidValue]['title'];
321 }
322 }
323 }
324 // Should we use join_pages instead of long lists of uids?
325 if ($this->conf['search.']['skipExtendToSubpagesChecking']) {
326 $this->join_pages = 1;
327 }
328 // Add media to search in:
329 if (trim($this->conf['search.']['mediaList']) !== '') {
330 $mediaList = implode(',', GeneralUtility::trimExplode(',', $this->conf['search.']['mediaList'], true));
331 }
332 foreach ($this->external_parsers as $extension => $obj) {
333 // Skip unwanted extensions
334 if ($mediaList && !GeneralUtility::inList($mediaList, $extension)) {
335 continue;
336 }
337 if ($name = $obj->searchTypeMediaTitle($extension)) {
338 $this->optValues['media'][$extension] = $this->pi_getLL('opt_sections_' . $extension, $name);
339 }
340 }
341 // Add operators for various languages
342 // Converts the operators to UTF-8 and lowercase
343 $this->operator_translate_table[] = array($this->charsetConverter->conv_case('utf-8', $this->charsetConverter->conv($this->pi_getLL('local_operator_AND'), $this->frontendController->renderCharset, 'utf-8'), 'toLower'), 'AND');
344 $this->operator_translate_table[] = array($this->charsetConverter->conv_case('utf-8', $this->charsetConverter->conv($this->pi_getLL('local_operator_OR'), $this->frontendController->renderCharset, 'utf-8'), 'toLower'), 'OR');
345 $this->operator_translate_table[] = array($this->charsetConverter->conv_case('utf-8', $this->charsetConverter->conv($this->pi_getLL('local_operator_NOT'), $this->frontendController->renderCharset, 'utf-8'), 'toLower'), 'AND NOT');
346 // This is the id of the site root. This value may be a commalist of integer (prepared for this)
347 $this->wholeSiteIdList = (int)$this->frontendController->config['rootLine'][0]['uid'];
348 // Creating levels for section menu:
349 // This selects the first and secondary menus for the "sections" selector - so we can search in sections and sub sections.
350 if ($this->conf['show.']['L1sections']) {
351 $firstLevelMenu = $this->getMenu($this->wholeSiteIdList);
352 foreach ($firstLevelMenu as $optionName => $mR) {
353 if (!$mR['nav_hide']) {
354 $this->optValues['sections']['rl1_' . $mR['uid']] = trim($this->pi_getLL('opt_RL1') . ' ' . $mR['title']);
355 if ($this->conf['show.']['L2sections']) {
356 $secondLevelMenu = $this->getMenu($mR['uid']);
357 foreach ($secondLevelMenu as $kk2 => $mR2) {
358 if (!$mR2['nav_hide']) {
359 $this->optValues['sections']['rl2_' . $mR2['uid']] = trim($this->pi_getLL('opt_RL2') . ' ' . $mR2['title']);
360 } else {
361 unset($secondLevelMenu[$kk2]);
362 }
363 }
364 $this->optValues['sections']['rl2_' . implode(',', array_keys($secondLevelMenu))] = $this->pi_getLL('opt_RL2ALL');
365 }
366 } else {
367 unset($firstLevelMenu[$optionName]);
368 }
369 }
370 $this->optValues['sections']['rl1_' . implode(',', array_keys($firstLevelMenu))] = $this->pi_getLL('opt_RL1ALL');
371 }
372 // Setting the list of root IDs for the search. Notice, these page IDs MUST have a TypoScript template with root flag on them! Basically this list is used to select on the "rl0" field and page ids are registered as "rl0" only if a TypoScript template record with root flag is there.
373 // This happens AFTER the use of $this->wholeSiteIdList above because the above will then fetch the menu for the CURRENT site - regardless of this kind of searching here. Thus a general search will lookup in the WHOLE database while a specific section search will take the current sections...
374 if ($this->conf['search.']['rootPidList']) {
375 $this->wholeSiteIdList = implode(',', GeneralUtility::intExplode(',', $this->conf['search.']['rootPidList']));
376 }
377 // Load the template
378 $this->templateCode = $this->cObj->fileResource($this->conf['templateFile']);
379 // Add search languages:
380 $res = $this->databaseConnection->exec_SELECTquery('*', 'sys_language', '1=1' . $this->cObj->enableFields('sys_language'));
381 while (false !== ($data = $this->databaseConnection->sql_fetch_assoc($res))) {
382 $this->optValues['lang'][$data['uid']] = $data['title'];
383 }
384 $this->databaseConnection->sql_free_result($res);
385 // Calling hook for modification of initialized content
386 if ($hookObj = $this->hookRequest('initialize_postProc')) {
387 $hookObj->initialize_postProc();
388 }
389 // Default values set:
390 // Setting first values in optValues as default values IF there is not corresponding piVar value set already.
391 foreach ($this->optValues as $optionName => $optionValue) {
392 if (!isset($this->piVars[$optionName])) {
393 reset($optionValue);
394 $this->piVars[$optionName] = key($optionValue);
395 }
396 }
397 // Blind selectors:
398 if (is_array($this->conf['blind.'])) {
399 foreach ($this->conf['blind.'] as $optionName => $optionValue) {
400 if (is_array($optionValue)) {
401 foreach ($optionValue as $optionValueSubKey => $optionValueSubValue) {
402 if (!is_array($optionValueSubValue) && $optionValueSubValue && is_array($this->optValues[substr($optionName, 0, -1)])) {
403 unset($this->optValues[substr($optionName, 0, -1)][$optionValueSubKey]);
404 }
405 }
406 } elseif ($optionValue) {
407 // If value is not set, unset the option array
408 unset($this->optValues[$optionName]);
409 }
410 }
411 }
412 // This gets the search-words into the $sWArr:
413 $this->sWArr = $this->getSearchWords($this->piVars['defOp']);
414 }
415
416 /**
417 * Splits the search word input into an array where each word is represented by an array with key "sword" holding the search word and key "oper" holding the SQL operator (eg. AND, OR)
418 *
419 * Only words with 2 or more characters are accepted
420 * Max 200 chars total
421 * Space is used to split words, "" can be used search for a whole string
422 * AND, OR and NOT are prefix words, overruling the default operator
423 * +/|/- equals AND, OR and NOT as operators.
424 * All search words are converted to lowercase.
425 *
426 * $defOp is the default operator. 1=OR, 0=AND
427 *
428 * @param bool $defOp If TRUE, the default operator will be OR, not AND
429 * @return array Returns array with search words if any found
430 */
431 public function getSearchWords($defOp)
432 {
433 // Shorten search-word string to max 200 bytes (does NOT take multibyte charsets into account - but never mind, shortening the string here is only a run-away feature!)
434 $inSW = substr($this->piVars['sword'], 0, 200);
435 // Convert to UTF-8 + conv. entities (was also converted during indexing!)
436 $inSW = $this->charsetConverter->conv($inSW, $this->frontendController->metaCharset, 'utf-8');
437 $inSW = $this->charsetConverter->entities_to_utf8($inSW, true);
438 $sWordArray = false;
439 if ($hookObj = $this->hookRequest('getSearchWords')) {
440 $sWordArray = $hookObj->getSearchWords_splitSWords($inSW, $defOp);
441 } else {
442 if ($this->piVars['type'] == 20) {
443 // type = Sentence
444 $sWordArray = array(array('sword' => trim($inSW), 'oper' => 'AND'));
445 } else {
446 $searchWords = \TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::getExplodedSearchString($inSW, $defOp == 1 ? 'OR' : 'AND', $this->operator_translate_table);
447 if (is_array($searchWords)) {
448 $sWordArray = $this->procSearchWordsByLexer($searchWords);
449 }
450 }
451 }
452 return $sWordArray;
453 }
454
455 /**
456 * Post-process the search word array so it will match the words that was indexed (including case-folding if any)
457 * If any words are split into multiple words (eg. CJK will be!) the operator of the main word will remain.
458 *
459 * @param array $SWArr Search word array
460 * @return array Search word array, processed through lexer
461 */
462 public function procSearchWordsByLexer($SWArr)
463 {
464 // Init output variable:
465 $newSWArr = array();
466 // Traverse the search word array:
467 foreach ($SWArr as $wordDef) {
468 if (!strstr($wordDef['sword'], ' ')) {
469 // No space in word (otherwise it might be a sentense in quotes like "there is").
470 // Split the search word by lexer:
471 $res = $this->lexerObj->split2Words($wordDef['sword']);
472 // Traverse lexer result and add all words again:
473 foreach ($res as $word) {
474 $newSWArr[] = array('sword' => $word, 'oper' => $wordDef['oper']);
475 }
476 } else {
477 $newSWArr[] = $wordDef;
478 }
479 }
480 // Return result:
481 return $newSWArr;
482 }
483
484 /*****************************
485 *
486 * Main functions
487 *
488 *****************************/
489 /**
490 * Performs the search, the display and writing stats
491 *
492 * @param array $sWArr Search words in array, see ->getSearchWords() for details
493 * @return string HTML for result display.
494 */
495 public function doSearch($sWArr)
496 {
497 // Find free index uid:
498 $freeIndexUid = $this->piVars['freeIndexUid'];
499 if ($freeIndexUid == -2) {
500 $freeIndexUid = $this->conf['search.']['defaultFreeIndexUidList'];
501 }
502 $indexCfgs = GeneralUtility::intExplode(',', $freeIndexUid);
503 $accumulatedContent = '';
504 foreach ($indexCfgs as $freeIndexUid) {
505 // Get result rows:
506 $pt1 = GeneralUtility::milliseconds();
507 if ($hookObj = $this->hookRequest('getResultRows')) {
508 $resData = $hookObj->getResultRows($sWArr, $freeIndexUid);
509 } else {
510 $resData = $this->getResultRows($sWArr, $freeIndexUid);
511 }
512 // Display search results:
513 $pt2 = GeneralUtility::milliseconds();
514 if ($hookObj = $this->hookRequest('getDisplayResults')) {
515 $content = $hookObj->getDisplayResults($sWArr, $resData, $freeIndexUid);
516 } else {
517 $content = $this->getDisplayResults($sWArr, $resData, $freeIndexUid);
518 }
519 $pt3 = GeneralUtility::milliseconds();
520 // Create header if we are searching more than one indexing configuration:
521 if (count($indexCfgs) > 1) {
522 if ($freeIndexUid > 0) {
523 $indexCfgRec = $this->databaseConnection->exec_SELECTgetSingleRow('title', 'index_config', 'uid=' . (int)$freeIndexUid . $this->cObj->enableFields('index_config'));
524 $titleString = $indexCfgRec['title'];
525 } else {
526 $titleString = $this->pi_getLL('opt_freeIndexUid_header_' . $freeIndexUid);
527 }
528 $content = '<h1 class="tx-indexedsearch-category">' . htmlspecialchars($titleString) . '</h1>' . $content;
529 }
530 $accumulatedContent .= $content;
531 }
532 // Write search statistics
533 $this->writeSearchStat($sWArr, $resData['count'], array($pt1, $pt2, $pt3));
534 // Return content:
535 return $accumulatedContent;
536 }
537
538 /**
539 * Get search result rows / data from database. Returned as data in array.
540 *
541 * @param array $searchWordArray Search word array
542 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
543 * @return array False if no result, otherwise an array with keys for first row, result rows and total number of results found.
544 */
545 public function getResultRows($searchWordArray, $freeIndexUid = -1)
546 {
547 // Getting SQL result pointer. This fetches ALL results (1,000,000 if found)
548 $this->timeTracker->push('Searching result');
549 if ($hookObj = &$this->hookRequest('getResultRows_SQLpointer')) {
550 $res = $hookObj->getResultRows_SQLpointer($searchWordArray, $freeIndexUid);
551 } else {
552 $res = $this->getResultRows_SQLpointer($searchWordArray, $freeIndexUid);
553 }
554 $this->timeTracker->pull();
555 // Organize and process result:
556 $result = false;
557 if ($res) {
558 $totalSearchResultCount = $this->databaseConnection->sql_num_rows($res);
559 // Total search-result count
560 $currentPageNumber = MathUtility::forceIntegerInRange($this->piVars['pointer'], 0, floor($totalSearchResultCount / $this->piVars['results']));
561 // The pointer is set to the result page that is currently being viewed
562 // Initialize result accumulation variables:
563 $positionInSearchResults = 0;
564 $groupingPhashes = array();
565 // Used for filtering out duplicates
566 $groupingChashes = array();
567 // Used for filtering out duplicates BASED ON cHash
568 $firstRow = array();
569 // Will hold the first row in result - used to calculate relative hit-ratings.
570 $resultRows = array();
571 // Will hold the results rows for display.
572 // Should we continue counting and checking of results even if
573 // we are sure they are not displayed in this request?
574 // This will slow down your page rendering, but it allows
575 // precise search result counters.
576 $calculateExactCount = (bool)$this->conf['search.']['exactCount'];
577 $lastResultNumberOnPreviousPage = $currentPageNumber * $this->piVars['results'];
578 $firstResultNumberOnNextPage = ($currentPageNumber + 1) * $this->piVars['results'];
579 $lastResultNumberToAnalyze = ($currentPageNumber + 1) * $this->piVars['results'] + $this->piVars['results'];
580 // Now, traverse result and put the rows to be displayed into an array
581 // Each row should contain the fields from 'ISEC.*, IP.*' combined + artificial fields "show_resume" (bool) and "result_number" (counter)
582 while (false !== ($row = $this->databaseConnection->sql_fetch_assoc($res))) {
583 // Set first row:
584 if ($positionInSearchResults === 0) {
585 $firstRow = $row;
586 }
587 $row['show_resume'] = $this->checkResume($row);
588 // Tells whether we can link directly to a document or not (depends on possible right problems)
589 $phashGr = !in_array($row['phash_grouping'], $groupingPhashes);
590 $chashGr = !in_array(($row['contentHash'] . '.' . $row['data_page_id']), $groupingChashes);
591 if ($phashGr && $chashGr) {
592 if ($row['show_resume'] || $this->conf['show.']['forbiddenRecords']) {
593 // Only if the resume may be shown are we going to filter out duplicates...
594 if (!$this->multiplePagesType($row['item_type'])) {
595 // Only on documents which are not multiple pages documents
596 $groupingPhashes[] = $row['phash_grouping'];
597 }
598 $groupingChashes[] = $row['contentHash'] . '.' . $row['data_page_id'];
599 $positionInSearchResults++;
600 // Check if we are inside result range for current page
601 if ($positionInSearchResults > $lastResultNumberOnPreviousPage && $positionInSearchResults <= $lastResultNumberToAnalyze) {
602 // Collect results to display
603 $row['result_number'] = $positionInSearchResults;
604 $resultRows[] = $row;
605 // This may lead to a problem: If the result
606 // check is not stopped here, the search will
607 // take longer. However the result counter
608 // will not filter out grouped cHashes/pHashes
609 // that were not processed yet. You can change
610 // this behavior using the "search.exactCount"
611 // property (see above).
612 $nextResultPosition = $positionInSearchResults + 1;
613 if (!$calculateExactCount && $nextResultPosition > $firstResultNumberOnNextPage) {
614 break;
615 }
616 }
617 } else {
618 // Skip this row if the user cannot view it (missing permission)
619 $totalSearchResultCount--;
620 }
621 } else {
622 // For each time a phash_grouping document is found
623 // (which is thus not displayed) the search-result count
624 // is reduced, so that it matches the number of rows displayed.
625 $totalSearchResultCount--;
626 }
627 }
628 $this->databaseConnection->sql_free_result($res);
629 $result = array(
630 'resultRows' => $resultRows,
631 'firstRow' => $firstRow,
632 'count' => $totalSearchResultCount
633 );
634 }
635 return $result;
636 }
637
638 /**
639 * Gets a SQL result pointer to traverse for the search records.
640 *
641 * @param array $sWArr Search words
642 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
643 * @return bool|\mysqli_result Query result pointer
644 */
645 public function getResultRows_SQLpointer($sWArr, $freeIndexUid = -1)
646 {
647 // This SEARCHES for the searchwords in $sWArr AND returns a COMPLETE list of phash-integers of the matches.
648 $list = $this->getPhashList($sWArr);
649 // Perform SQL Search / collection of result rows array:
650 if ($list) {
651 // Do the search:
652 $this->timeTracker->push('execFinalQuery');
653 $res = $this->execFinalQuery($list, $freeIndexUid);
654 $this->timeTracker->pull();
655 return $res;
656 } else {
657 return false;
658 }
659 }
660
661 /**
662 * Compiles the HTML display of the incoming array of result rows.
663 *
664 * @param array $sWArr Search words array (for display of text describing what was searched for)
665 * @param array $resData Array with result rows, count, first row.
666 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
667 * @return string HTML content to display result.
668 */
669 public function getDisplayResults($sWArr, $resData, $freeIndexUid = -1)
670 {
671 $content = '';
672 // Perform display of result rows array:
673 if ($resData) {
674 $this->timeTracker->push('Display Final result');
675 // Set first selected row (for calculation of ranking later)
676 $this->firstRow = $resData['firstRow'];
677 // Result display here:
678 $rowcontent = '';
679 $rowcontent .= $this->compileResult($resData['resultRows'], $freeIndexUid);
680 // Browsing box:
681 if ($resData['count']) {
682 $this->internal['res_count'] = $resData['count'];
683 $this->internal['results_at_a_time'] = $this->piVars['results'];
684 $this->internal['maxPages'] = MathUtility::forceIntegerInRange($this->conf['search.']['page_links'], 1, 100, 10);
685 $resultSectionsCount = count($this->resultSections);
686 $addString = $resData['count'] && $this->piVars['group'] == 'sections' && $freeIndexUid <= 0 ? ' ' . sprintf($this->pi_getLL(($resultSectionsCount > 1 ? 'inNsections' : 'inNsection')), $resultSectionsCount) : '';
687 $browseBox1 = $this->pi_list_browseresults(1, $addString, $this->printResultSectionLinks(), $freeIndexUid);
688 $browseBox2 = $this->pi_list_browseresults(0, '', '', $freeIndexUid);
689 // Browsing nav, bottom.
690 $content = $browseBox1 . $rowcontent . $browseBox2;
691 } else {
692 $content = '<p' . $this->pi_classParam('noresults') . '>' . $this->pi_getLL('noResults', '', true) . '</p>';
693 }
694 $this->timeTracker->pull();
695 } else {
696 $content .= '<p' . $this->pi_classParam('noresults') . '>' . $this->pi_getLL('noResults', '', true) . '</p>';
697 }
698 // Print a message telling which words we searched for, and in which sections etc.
699 $what = $this->tellUsWhatIsSeachedFor($sWArr) . (substr($this->piVars['sections'], 0, 2) == 'rl' ? ' ' . $this->pi_getLL('inSection', '', true) . ' "' . $this->getPathFromPageId(substr($this->piVars['sections'], 4)) . '"' : '');
700 $what = '<div' . $this->pi_classParam('whatis') . '>' . $this->cObj->stdWrap($what, $this->conf['whatis_stdWrap.']) . '</div>';
701 $content = $what . $content;
702 // Return content:
703 return $content;
704 }
705
706 /**
707 * Takes the array with resultrows as input and returns the result-HTML-code
708 * Takes the "group" var into account: Makes a "section" or "flat" display.
709 *
710 * @param array $resultRows Result rows
711 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
712 * @return string HTML
713 */
714 public function compileResult($resultRows, $freeIndexUid = -1)
715 {
716 $content = '';
717 // Transfer result rows to new variable, performing some mapping of sub-results etc.
718 $newResultRows = array();
719 foreach ($resultRows as $row) {
720 $id = md5($row['phash_grouping']);
721 if (is_array($newResultRows[$id])) {
722 if (!$newResultRows[$id]['show_resume'] && $row['show_resume']) {
723 // swapping:
724 // Remove old
725 $subrows = $newResultRows[$id]['_sub'];
726 unset($newResultRows[$id]['_sub']);
727 $subrows[] = $newResultRows[$id];
728 // Insert new:
729 $newResultRows[$id] = $row;
730 $newResultRows[$id]['_sub'] = $subrows;
731 } else {
732 $newResultRows[$id]['_sub'][] = $row;
733 }
734 } else {
735 $newResultRows[$id] = $row;
736 }
737 }
738 $resultRows = $newResultRows;
739 $this->resultSections = array();
740 if ($freeIndexUid <= 0) {
741 switch ($this->piVars['group']) {
742 case 'sections':
743 $rl2flag = substr($this->piVars['sections'], 0, 2) == 'rl';
744 $sections = array();
745 foreach ($resultRows as $row) {
746 $id = $row['rl0'] . '-' . $row['rl1'] . ($rl2flag ? '-' . $row['rl2'] : '');
747 $sections[$id][] = $row;
748 }
749 $this->resultSections = array();
750 foreach ($sections as $id => $resultRows) {
751 $rlParts = explode('-', $id);
752 $theId = $rlParts[2] ? $rlParts[2] : ($rlParts[1] ? $rlParts[1] : $rlParts[0]);
753 $theRLid = $rlParts[2] ? 'rl2_' . $rlParts[2] : ($rlParts[1] ? 'rl1_' . $rlParts[1] : '0');
754 $sectionName = $this->getPathFromPageId($theId);
755 if ($sectionName[0] == '/') {
756 $sectionName = substr($sectionName, 1);
757 }
758 if (!trim($sectionName)) {
759 $sectionTitleLinked = $this->pi_getLL('unnamedSection', '', true) . ':';
760 } elseif ($this->conf['linkSectionTitles']) {
761 $quotedPrefix = GeneralUtility::quoteJSvalue($this->prefixId);
762 $onclick = 'document.forms[' . $quotedPrefix . '][' . GeneralUtility::quoteJSvalue($this->prefixId . '[_sections]') . '].value=' . GeneralUtility::quoteJSvalue($theRLid) . ';document.forms[' . $quotedPrefix . '].submit();return false;';
763 $sectionTitleLinked = '<a href="#" onclick="' . htmlspecialchars($onclick) . '">' . $sectionName . ':</a>';
764 } else {
765 $sectionTitleLinked = $sectionName;
766 }
767 $resultRowsCount = count($resultRows);
768 $this->resultSections[$id] = array($sectionName, $resultRowsCount);
769 // Add content header:
770 $content .= $this->makeSectionHeader($id, $sectionTitleLinked, $resultRowsCount);
771 // Render result rows:
772 $resultlist = '';
773 foreach ($resultRows as $row) {
774 $resultlist .= $this->printResultRow($row);
775 }
776 $content .= $this->cObj->stdWrap($resultlist, $this->conf['resultlist_stdWrap.']);
777 }
778 break;
779 default:
780 // flat:
781 $resultlist = '';
782 foreach ($resultRows as $row) {
783 $resultlist .= $this->printResultRow($row);
784 }
785 $content .= $this->cObj->stdWrap($resultlist, $this->conf['resultlist_stdWrap.']);
786 }
787 } else {
788 $resultlist = '';
789 foreach ($resultRows as $row) {
790 $resultlist .= $this->printResultRow($row);
791 }
792 $content .= $this->cObj->stdWrap($resultlist, $this->conf['resultlist_stdWrap.']);
793 }
794 return '<div' . $this->pi_classParam('res') . '>' . $content . '</div>';
795 }
796
797 /***********************************
798 *
799 * Searching functions (SQL)
800 *
801 ***********************************/
802 /**
803 * Returns a COMPLETE list of phash-integers matching the search-result composed of the search-words in the sWArr array.
804 * The list of phash integers are unsorted and should be used for subsequent selection of index_phash records for display of the result.
805 *
806 * @param array $sWArr Search word array
807 * @return string List of integers
808 */
809 public function getPhashList($sWArr)
810 {
811 // Initialize variables:
812 $c = 0;
813 $totalHashList = array();
814 // This array accumulates the phash-values
815 // Traverse searchwords; for each, select all phash integers and merge/diff/intersect them with previous word (based on operator)
816 foreach ($sWArr as $k => $v) {
817 // Making the query for a single search word based on the search-type
818 $sWord = $v['sword'];
819 $theType = (string)$this->piVars['type'];
820 if (strstr($sWord, ' ')) {
821 // If there are spaces in the search-word, make a full text search instead.
822 $theType = 20;
823 }
824 $this->timeTracker->push('SearchWord "' . $sWord . '" - $theType=' . $theType);
825 // Perform search for word:
826 switch ($theType) {
827 case '1':
828 // Part of word
829 $res = $this->searchWord($sWord, Utility\LikeWildcard::BOTH);
830 break;
831 case '2':
832 // First part of word
833 $res = $this->searchWord($sWord, Utility\LikeWildcard::RIGHT);
834 break;
835 case '3':
836 // Last part of word
837 $res = $this->searchWord($sWord, Utility\LikeWildcard::LEFT);
838 break;
839 case '10':
840 // Sounds like
841 /**
842 * Indexer object
843 *
844 * @var \TYPO3\CMS\IndexedSearch\Indexer
845 */
846 // Initialize the indexer-class
847 $indexerObj = GeneralUtility::makeInstance(\TYPO3\CMS\IndexedSearch\Indexer::class);
848 // Perform metaphone search
849 $res = $this->searchMetaphone($indexerObj->metaphone($sWord, $this->storeMetaphoneInfoAsWords));
850 unset($indexerObj);
851 break;
852 case '20':
853 // Sentence
854 $res = $this->searchSentence($sWord);
855 $this->piVars['order'] = 'mtime';
856 // If there is a fulltext search for a sentence there is a likeliness that sorting cannot be done by the rankings from the rel-table (because no relations will exist for the sentence in the word-table). So therefore mtime is used instead. It is not required, but otherwise some hits may be left out.
857 break;
858 default:
859 // Distinct word
860 $res = $this->searchDistinct($sWord);
861 }
862 // If there was a query to do, then select all phash-integers which resulted from this.
863 if ($res) {
864 // Get phash list by searching for it:
865 $phashList = array();
866 while ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
867 $phashList[] = $row['phash'];
868 }
869 $this->databaseConnection->sql_free_result($res);
870 // Here the phash list are merged with the existing result based on whether we are dealing with OR, NOT or AND operations.
871 if ($c) {
872 switch ($v['oper']) {
873 case 'OR':
874 $totalHashList = array_unique(array_merge($phashList, $totalHashList));
875 break;
876 case 'AND NOT':
877 $totalHashList = array_diff($totalHashList, $phashList);
878 break;
879 default:
880 // AND...
881 $totalHashList = array_intersect($totalHashList, $phashList);
882 }
883 } else {
884 $totalHashList = $phashList;
885 }
886 }
887 $this->timeTracker->pull();
888 $c++;
889 }
890 return implode(',', $totalHashList);
891 }
892
893 /**
894 * Returns a query which selects the search-word from the word/rel tables.
895 *
896 * @param string $wordSel WHERE clause selecting the word from phash
897 * @param string $plusQ Additional AND clause in the end of the query.
898 * @return bool|\mysqli_result SQL result pointer
899 */
900 public function execPHashListQuery($wordSel, $plusQ = '')
901 {
902 return $this->databaseConnection->exec_SELECTquery('IR.phash', 'index_words IW,
903 index_rel IR,
904 index_section ISEC', $wordSel . '
905 AND IW.wid=IR.wid
906 AND ISEC.phash = IR.phash
907 ' . $this->sectionTableWhere() . '
908 ' . $plusQ, 'IR.phash');
909 }
910
911 /**
912 * Search for a word
913 *
914 * @param string $sWord Word to search for
915 * @param int $wildcard Bit-field of Utility\LikeWildcard
916 * @return bool|\mysqli_result SQL result pointer
917 */
918 public function searchWord($sWord, $wildcard)
919 {
920 $likeWildcard = Utility\LikeWildcard::cast($wildcard);
921 $wSel = $likeWildcard->getLikeQueryPart(
922 'index_words',
923 'IW.baseword',
924 $sWord
925 );
926
927 $this->wSelClauses[] = $wSel;
928 $res = $this->execPHashListQuery($wSel, ' AND is_stopword=0');
929 return $res;
930 }
931
932 /**
933 * Search for one distinct word
934 *
935 * @param string $sWord Word to search for
936 * @return bool|\mysqli_result SQL result pointer
937 */
938 public function searchDistinct($sWord)
939 {
940 $wSel = 'IW.wid=' . \TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::md5inthash($sWord);
941 $this->wSelClauses[] = $wSel;
942 $res = $this->execPHashListQuery($wSel, ' AND is_stopword=0');
943 return $res;
944 }
945
946 /**
947 * Search for a sentence
948 *
949 * @param string $sSentence Sentence to search for
950 * @return bool|\mysqli_result SQL result pointer
951 */
952 public function searchSentence($sSentence)
953 {
954 $this->wSelClauses[] = '1=1';
955 $likeWildcard = Utility\LikeWildcard::cast(Utility\LikeWildcard::BOTH);
956 $likePart = $likeWildcard->getLikeQueryPart(
957 'index_fulltext',
958 'IFT.fulltextdata',
959 $sSentence
960 );
961
962 return $this->databaseConnection->exec_SELECTquery('ISEC.phash',
963 'index_section ISEC, index_fulltext IFT',
964 $likePart . ' AND ISEC.phash = IFT.phash' . $this->sectionTableWhere(), 'ISEC.phash'
965 );
966 }
967
968 /**
969 * Search for a metaphone word
970 *
971 * @param string $sWord Word to search for
972 * @return \mysqli_result SQL result pointer
973 */
974 public function searchMetaphone($sWord)
975 {
976 $wSel = 'IW.metaphone=' . $sWord;
977 $this->wSelClauses[] = $wSel;
978 return $this->execPHashListQuery($wSel, ' AND is_stopword=0');
979 }
980
981 /**
982 * Returns AND statement for selection of section in database. (rootlevel 0-2 + page_id)
983 *
984 * @return string AND clause for selection of section in database.
985 */
986 public function sectionTableWhere()
987 {
988 $out = $this->wholeSiteIdList < 0 ? '' : ' AND ISEC.rl0 IN (' . $this->wholeSiteIdList . ')';
989 $match = '';
990 if (substr($this->piVars['sections'], 0, 4) == 'rl1_') {
991 $list = implode(',', GeneralUtility::intExplode(',', substr($this->piVars['sections'], 4)));
992 $out .= ' AND ISEC.rl1 IN (' . $list . ')';
993 $match = true;
994 } elseif (substr($this->piVars['sections'], 0, 4) == 'rl2_') {
995 $list = implode(',', GeneralUtility::intExplode(',', substr($this->piVars['sections'], 4)));
996 $out .= ' AND ISEC.rl2 IN (' . $list . ')';
997 $match = true;
998 } elseif (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'])) {
999 // Traversing user configured fields to see if any of those are used to limit search to a section:
1000 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'] as $fieldName => $rootLineLevel) {
1001 if (substr($this->piVars['sections'], 0, strlen($fieldName) + 1) == $fieldName . '_') {
1002 $list = implode(',', GeneralUtility::intExplode(',', substr($this->piVars['sections'], strlen($fieldName) + 1)));
1003 $out .= ' AND ISEC.' . $fieldName . ' IN (' . $list . ')';
1004 $match = true;
1005 break;
1006 }
1007 }
1008 }
1009 // If no match above, test the static types:
1010 if (!$match) {
1011 switch ((string)$this->piVars['sections']) {
1012 case '-1':
1013 // '-1' => 'Only this page',
1014 $out .= ' AND ISEC.page_id=' . $this->frontendController->id;
1015 break;
1016 case '-2':
1017 // '-2' => 'Top + level 1',
1018 $out .= ' AND ISEC.rl2=0';
1019 break;
1020 case '-3':
1021 // '-3' => 'Level 2 and out',
1022 $out .= ' AND ISEC.rl2>0';
1023 break;
1024 }
1025 }
1026 return $out;
1027 }
1028
1029 /**
1030 * Returns AND statement for selection of media type
1031 *
1032 * @return string AND statement for selection of media type
1033 */
1034 public function mediaTypeWhere()
1035 {
1036 switch ((string)$this->piVars['media']) {
1037 case '0':
1038 // '0' => 'Kun TYPO3 sider',
1039 $out = ' AND IP.item_type=' . $this->databaseConnection->fullQuoteStr('0', 'index_phash');
1040 break;
1041 case '-2':
1042 // All external documents
1043 $out = ' AND IP.item_type<>' . $this->databaseConnection->fullQuoteStr('0', 'index_phash');
1044 break;
1045 case '-1':
1046 // All content
1047 $out = '';
1048 break;
1049 default:
1050 $out = ' AND IP.item_type=' . $this->databaseConnection->fullQuoteStr($this->piVars['media'], 'index_phash');
1051 }
1052 return $out;
1053 }
1054
1055 /**
1056 * Returns AND statement for selection of language
1057 *
1058 * @return string AND statement for selection of language
1059 */
1060 public function languageWhere()
1061 {
1062 $languageWhere = '';
1063 if ($this->piVars['lang'] >= 0) {
1064 // -1 is the same as ALL language.
1065 $languageWhere = 'AND IP.sys_language_uid=' . (int)$this->piVars['lang'];
1066 }
1067 return $languageWhere;
1068 }
1069
1070 /**
1071 * Where-clause for free index-uid value.
1072 *
1073 * @param int $freeIndexUid Free Index UID value to limit search to.
1074 * @return string WHERE SQL clause part.
1075 */
1076 public function freeIndexUidWhere($freeIndexUid)
1077 {
1078 if ($freeIndexUid < 0) {
1079 return '';
1080 }
1081 // First, look if the freeIndexUid is a meta configuration:
1082 $indexCfgRec = $this->databaseConnection->exec_SELECTgetSingleRow('indexcfgs', 'index_config', 'type=5 AND uid=' . (int)$freeIndexUid . $this->cObj->enableFields('index_config'));
1083 if (is_array($indexCfgRec)) {
1084 $refs = GeneralUtility::trimExplode(',', $indexCfgRec['indexcfgs']);
1085 $list = array(-99);
1086 // Default value to protect against empty array.
1087 foreach ($refs as $ref) {
1088 list($table, $uid) = GeneralUtility::revExplode('_', $ref, 2);
1089 switch ($table) {
1090 case 'index_config':
1091 $idxRec = $this->databaseConnection->exec_SELECTgetSingleRow('uid', 'index_config', 'uid=' . (int)$uid . $this->cObj->enableFields('index_config'));
1092 if ($idxRec) {
1093 $list[] = $uid;
1094 }
1095 break;
1096 case 'pages':
1097 $indexCfgRecordsFromPid = $this->databaseConnection->exec_SELECTgetRows('uid', 'index_config', 'pid=' . (int)$uid . $this->cObj->enableFields('index_config'));
1098 foreach ($indexCfgRecordsFromPid as $idxRec) {
1099 $list[] = $idxRec['uid'];
1100 }
1101 break;
1102 }
1103 }
1104 $list = array_unique($list);
1105 } else {
1106 $list = array((int)$freeIndexUid);
1107 }
1108 return ' AND IP.freeIndexUid IN (' . implode(',', $list) . ')';
1109 }
1110
1111 /**
1112 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
1113 *
1114 * @param string $list List of phash integers which match the search.
1115 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
1116 * @return bool|\mysqli_result Query result pointer
1117 */
1118 public function execFinalQuery($list, $freeIndexUid = -1)
1119 {
1120 // Setting up methods of filtering results based on page types, access, etc.
1121 $page_join = '';
1122 $page_where = '';
1123 // Calling hook for alternative creation of page ID list
1124 if ($hookObj = $this->hookRequest('execFinalQuery_idList')) {
1125 $page_where = $hookObj->execFinalQuery_idList($list);
1126 } elseif ($this->join_pages) {
1127 // Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
1128 $page_join = ',
1129 pages';
1130 $page_where = 'pages.uid = ISEC.page_id
1131 ' . $this->cObj->enableFields('pages') . '
1132 AND pages.no_search=0
1133 AND pages.doktype<200
1134 ';
1135 } elseif ($this->wholeSiteIdList >= 0) {
1136 // Collecting all pages IDs in which to search; filtering out ALL pages that are not accessible due to enableFields. Does NOT look for "no_search" field!
1137 $siteIdNumbers = GeneralUtility::intExplode(',', $this->wholeSiteIdList);
1138 $id_list = array();
1139 foreach ($siteIdNumbers as $rootId) {
1140 $id_list[] = $this->cObj->getTreeList(-1 * $rootId, 9999);
1141 }
1142 $page_where = 'ISEC.page_id IN (' . implode(',', $id_list) . ')';
1143 } else {
1144 // Disable everything... (select all)
1145 $page_where = '1=1';
1146 }
1147 // Indexing configuration clause:
1148 $freeIndexUidClause = $this->freeIndexUidWhere($freeIndexUid);
1149 // If any of the ranking sortings are selected, we must make a join with the word/rel-table again, because we need to calculate ranking based on all search-words found.
1150 if (substr($this->piVars['order'], 0, 5) == 'rank_') {
1151 switch ($this->piVars['order']) {
1152 case 'rank_flag':
1153 // This gives priority to word-position (max-value) so that words in title, keywords, description counts more than in content.
1154 // The ordering is refined with the frequency sum as well.
1155 $grsel = 'MAX(IR.flags) AS order_val1, SUM(IR.freq) AS order_val2';
1156 $orderBy = 'order_val1' . $this->isDescending() . ',order_val2' . $this->isDescending();
1157 break;
1158 case 'rank_first':
1159 // Results in average position of search words on page. Must be inversely sorted (low numbers are closer to top)
1160 $grsel = 'AVG(IR.first) AS order_val';
1161 $orderBy = 'order_val' . $this->isDescending(1);
1162 break;
1163 case 'rank_count':
1164 // Number of words found
1165 $grsel = 'SUM(IR.count) AS order_val';
1166 $orderBy = 'order_val' . $this->isDescending();
1167 break;
1168 default:
1169 // Frequency sum. I'm not sure if this is the best way to do it (make a sum...). Or should it be the average?
1170 $grsel = 'SUM(IR.freq) AS order_val';
1171 $orderBy = 'order_val' . $this->isDescending();
1172 }
1173
1174 // So, words are imploded into an OR statement (no "sentence search" should be done here - may deselect results)
1175 $wordSel = '(' . implode(' OR ', $this->wSelClauses) . ') AND ';
1176
1177 $res = $this->databaseConnection->exec_SELECTquery(
1178 'ISEC.*, IP.*, ' . $grsel,
1179 'index_words IW,
1180 index_rel IR,
1181 index_section ISEC,
1182 index_phash IP' . $page_join,
1183 $wordSel .
1184 'IP.phash IN (' . $list . ') ' .
1185 $this->mediaTypeWhere() . ' ' . $this->languageWhere() . $freeIndexUidClause . '
1186 AND IW.wid=IR.wid
1187 AND ISEC.phash = IR.phash
1188 AND IP.phash = IR.phash
1189 AND ' . $page_where,
1190 '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',
1191 $orderBy
1192 );
1193 } else {
1194 // Otherwise, if sorting are done with the pages table or other fields, there is no need for joining with the rel/word tables:
1195 $orderBy = '';
1196 switch ((string)$this->piVars['order']) {
1197 case 'title':
1198 $orderBy = 'IP.item_title' . $this->isDescending();
1199 break;
1200 case 'crdate':
1201 $orderBy = 'IP.item_crdate' . $this->isDescending();
1202 break;
1203 case 'mtime':
1204 $orderBy = 'IP.item_mtime' . $this->isDescending();
1205 break;
1206 }
1207 $res = $this->databaseConnection->exec_SELECTquery('ISEC.*, IP.*', 'index_phash IP,index_section ISEC' . $page_join, 'IP.phash IN (' . $list . ') ' . $this->mediaTypeWhere() . ' ' . $this->languageWhere() . $freeIndexUidClause . '
1208 AND IP.phash = ISEC.phash
1209 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);
1210 }
1211 return $res;
1212 }
1213
1214 /**
1215 * Checking if the resume can be shown for the search result (depending on whether the rights are OK)
1216 * ? Should it also check for gr_list "0,-1"?
1217 *
1218 * @param array $row Result row array.
1219 * @return bool Returns TRUE if resume can safely be shown
1220 */
1221 public function checkResume($row)
1222 {
1223 // If the record is indexed by an indexing configuration, just show it.
1224 // At least this is needed for external URLs and files.
1225 // For records we might need to extend this - for instance block display if record is access restricted.
1226 if ($row['freeIndexUid']) {
1227 return true;
1228 }
1229 // Evaluate regularly indexed pages based on item_type:
1230 if ($row['item_type']) {
1231 // External media:
1232 // For external media we will check the access of the parent page on which the media was linked from.
1233 // "phash_t3" is the phash of the parent TYPO3 page row which initiated the indexing of the documents in this section.
1234 // So, selecting for the grlist records belonging to the parent phash-row where the current users gr_list exists will help us to know.
1235 // 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.
1236 if (\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_grlist')) {
1237 $res = $this->databaseConnection->exec_SELECTquery('phash', 'index_grlist', 'phash=' . (int)$row['phash_t3'] . ' AND gr_list=' . $this->databaseConnection->fullQuoteStr($this->frontendController->gr_list, 'index_grlist'));
1238 } else {
1239 $res = false;
1240 }
1241 if ($res && $this->databaseConnection->sql_num_rows($res)) {
1242 return true;
1243 } else {
1244 return false;
1245 }
1246 } else {
1247 // Ordinary TYPO3 pages:
1248 if ((string)$row['gr_list'] !== (string)$this->frontendController->gr_list) {
1249 // 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...
1250 if (\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_grlist')) {
1251 $res = $this->databaseConnection->exec_SELECTquery('phash', 'index_grlist', 'phash=' . (int)$row['phash'] . ' AND gr_list=' . $this->databaseConnection->fullQuoteStr($this->frontendController->gr_list, 'index_grlist'));
1252 } else {
1253 $res = false;
1254 }
1255 if ($res && $this->databaseConnection->sql_num_rows($res)) {
1256 return true;
1257 } else {
1258 return false;
1259 }
1260 } else {
1261 return true;
1262 }
1263 }
1264 }
1265
1266 /**
1267 * Returns "DESC" or "" depending on the settings of the incoming highest/lowest result order (piVars['desc']
1268 *
1269 * @param bool $inverse If TRUE, inverse the order which is defined by piVars['desc']
1270 * @return string " DESC" or
1271 */
1272 public function isDescending($inverse = false)
1273 {
1274 $desc = $this->piVars['desc'];
1275 if ($inverse) {
1276 $desc = !$desc;
1277 }
1278 return !$desc ? ' DESC' : '';
1279 }
1280
1281 /**
1282 * Write statistics information to database for the search operation
1283 *
1284 * @param array $sWArr Search Word array
1285 * @param int $count Number of hits
1286 * @param int $pt Milliseconds the search took
1287 * @return void
1288 */
1289 public function writeSearchStat($sWArr, $count, $pt)
1290 {
1291 $insertFields = array(
1292 'searchstring' => $this->piVars['sword'],
1293 'searchoptions' => serialize(array($this->piVars, $sWArr, $pt)),
1294 'feuser_id' => (int)$GLOBALS['TSFE']->user['uid'],
1295 // fe_user id, integer
1296 'cookie' => (string)$GLOBALS['TSFE']->id,
1297 // cookie as set or retrieve. If people has cookies disabled this will vary all the time...
1298 'IP' => GeneralUtility::getIndpEnv('REMOTE_ADDR'),
1299 // Remote IP address
1300 'hits' => (int)$count,
1301 // Number of hits on the search.
1302 'tstamp' => $GLOBALS['EXEC_TIME']
1303 );
1304 $this->databaseConnection->exec_INSERTquery('index_stat_search', $insertFields);
1305 $newId = $this->databaseConnection->sql_insert_id();
1306 if ($newId) {
1307 foreach ($sWArr as $val) {
1308 $insertFields = array(
1309 'word' => $val['sword'],
1310 'index_stat_search_id' => $newId,
1311 'tstamp' => $GLOBALS['EXEC_TIME'],
1312 // Time stamp
1313 'pageid' => $this->frontendController->id
1314 );
1315 $this->databaseConnection->exec_INSERTquery('index_stat_word', $insertFields);
1316 }
1317 }
1318 }
1319
1320 /***********************************
1321 *
1322 * HTML output functions
1323 *
1324 ***********************************/
1325 /**
1326 * Make search form HTML
1327 *
1328 * @param array $optValues Value/Labels pairs for search form selector boxes.
1329 * @return string Search form HTML
1330 */
1331 public function makeSearchForm($optValues)
1332 {
1333 $html = $this->cObj->getSubpart($this->templateCode, '###SEARCH_FORM###');
1334 // Multilangual text
1335 $substituteArray = array('legend', 'searchFor', 'extResume', 'atATime', 'orderBy', 'fromSection', 'searchIn', 'match', 'style', 'freeIndexUid');
1336 foreach ($substituteArray as $marker) {
1337 $markerArray['###FORM_' . GeneralUtility::strtoupper($marker) . '###'] = $this->pi_getLL('form_' . $marker, '', true);
1338 }
1339 $markerArray['###FORM_SUBMIT###'] = $this->pi_getLL('submit_button_label', '', true);
1340 // Adding search field value
1341 $markerArray['###SWORD_VALUE###'] = '';
1342 $markerArray['###PLACEHOLDER###'] = '';
1343 if (!empty($this->piVars['sword'])) {
1344 $markerArray['###SWORD_VALUE###'] = htmlspecialchars($this->piVars['sword']);
1345 } else {
1346 // Add a HTML5 placeholder attribute if the configured doctype allows it
1347 if ($GLOBALS['TSFE']->config['config']['doctype'] === 'html5') {
1348 $markerArray['###PLACEHOLDER###'] = 'placeholder="' . $this->pi_getLL('default_search_word_entry') . '"';
1349 } else {
1350 $markerArray['###SWORD_VALUE###'] = $this->pi_getLL('default_search_word_entry');
1351 }
1352 }
1353 // Additonal keyword => "Add to current search words"
1354 if ($this->conf['show.']['clearSearchBox'] && $this->conf['show.']['clearSearchBox.']['enableSubSearchCheckBox']) {
1355 $markerArray['###SWORD_PREV_VALUE###'] = htmlspecialchars($this->conf['show.']['clearSearchBox'] ? '' : $this->piVars['sword']);
1356 $markerArray['###SWORD_PREV_INCLUDE_CHECKED###'] = $this->piVars['sword_prev_include'] ? ' checked="checked"' : '';
1357 $markerArray['###ADD_TO_CURRENT_SEARCH###'] = $this->pi_getLL('makerating_addToCurrentSearch', '', true);
1358 } else {
1359 $html = $this->cObj->substituteSubpart($html, '###ADDITONAL_KEYWORD###', '');
1360 }
1361 $markerArray['###ACTION_URL###'] = htmlspecialchars($this->getSearchFormActionURL());
1362 $hiddenFieldCode = $this->cObj->getSubpart($this->templateCode, '###HIDDEN_FIELDS###');
1363 $hiddenFieldCode = preg_replace('/^\\n\\t(.+)/ms', '$1', $hiddenFieldCode);
1364 // Remove first newline and tab (cosmetical issue)
1365 $hiddenFieldArr = array();
1366 foreach (GeneralUtility::trimExplode(',', $this->hiddenFieldList) as $fieldName) {
1367 $hiddenFieldMarkerArray = array();
1368 $hiddenFieldMarkerArray['###HIDDEN_FIELDNAME###'] = $this->prefixId . '[' . $fieldName . ']';
1369 $hiddenFieldMarkerArray['###HIDDEN_VALUE###'] = htmlspecialchars((string)$this->piVars[$fieldName]);
1370 $hiddenFieldArr[$fieldName] = $this->cObj->substituteMarkerArrayCached($hiddenFieldCode, $hiddenFieldMarkerArray, array(), array());
1371 }
1372 // Extended search
1373 if ($this->piVars['ext']) {
1374 // Search for
1375 if (!is_array($optValues['type']) && !is_array($optValues['defOp']) || $this->conf['blind.']['type'] && $this->conf['blind.']['defOp']) {
1376 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_FOR###', '');
1377 } else {
1378 if (is_array($optValues['type']) && !$this->conf['blind.']['type']) {
1379 unset($hiddenFieldArr['type']);
1380 $markerArray['###SELECTBOX_TYPE_VALUES###'] = $this->renderSelectBoxValues($this->piVars['type'], $optValues['type']);
1381 } else {
1382 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_TYPE###', '');
1383 }
1384 if (is_array($optValues['defOp']) || !$this->conf['blind.']['defOp']) {
1385 $markerArray['###SELECTBOX_DEFOP_VALUES###'] = $this->renderSelectBoxValues($this->piVars['defOp'], $optValues['defOp']);
1386 } else {
1387 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_DEFOP###', '');
1388 }
1389 }
1390 // Search in
1391 if (!is_array($optValues['media']) && !is_array($optValues['lang']) || $this->conf['blind.']['media'] && $this->conf['blind.']['lang']) {
1392 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_IN###', '');
1393 } else {
1394 if (is_array($optValues['media']) && !$this->conf['blind.']['media']) {
1395 unset($hiddenFieldArr['media']);
1396 $markerArray['###SELECTBOX_MEDIA_VALUES###'] = $this->renderSelectBoxValues($this->piVars['media'], $optValues['media']);
1397 } else {
1398 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_MEDIA###', '');
1399 }
1400 if (is_array($optValues['lang']) || !$this->conf['blind.']['lang']) {
1401 unset($hiddenFieldArr['lang']);
1402 $markerArray['###SELECTBOX_LANG_VALUES###'] = $this->renderSelectBoxValues($this->piVars['lang'], $optValues['lang']);
1403 } else {
1404 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_LANG###', '');
1405 }
1406 }
1407 // Sections
1408 if (!is_array($optValues['sections']) || $this->conf['blind.']['sections']) {
1409 $html = $this->cObj->substituteSubpart($html, '###SELECT_SECTION###', '');
1410 } else {
1411 $markerArray['###SELECTBOX_SECTIONS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['sections'], $optValues['sections']);
1412 }
1413 // Free Indexing Configurations:
1414 if (!is_array($optValues['freeIndexUid']) || $this->conf['blind.']['freeIndexUid']) {
1415 $html = $this->cObj->substituteSubpart($html, '###SELECT_FREEINDEXUID###', '');
1416 } else {
1417 $markerArray['###SELECTBOX_FREEINDEXUIDS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['freeIndexUid'], $optValues['freeIndexUid']);
1418 }
1419 // Sorting
1420 if (!is_array($optValues['order']) || !is_array($optValues['desc']) || $this->conf['blind.']['order']) {
1421 $html = $this->cObj->substituteSubpart($html, '###SELECT_ORDER###', '');
1422 } else {
1423 unset($hiddenFieldArr['order']);
1424 unset($hiddenFieldArr['desc']);
1425 unset($hiddenFieldArr['results']);
1426 $markerArray['###SELECTBOX_ORDER_VALUES###'] = $this->renderSelectBoxValues($this->piVars['order'], $optValues['order']);
1427 $markerArray['###SELECTBOX_DESC_VALUES###'] = $this->renderSelectBoxValues($this->piVars['desc'], $optValues['desc']);
1428 $markerArray['###SELECTBOX_RESULTS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['results'], $optValues['results']);
1429 }
1430 // Limits
1431 if (!is_array($optValues['results']) || !is_array($optValues['results']) || $this->conf['blind.']['results']) {
1432 $html = $this->cObj->substituteSubpart($html, '###SELECT_RESULTS###', '');
1433 } else {
1434 $markerArray['###SELECTBOX_RESULTS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['results'], $optValues['results']);
1435 }
1436 // Grouping
1437 if (!is_array($optValues['group']) || $this->conf['blind.']['group']) {
1438 $html = $this->cObj->substituteSubpart($html, '###SELECT_GROUP###', '');
1439 } else {
1440 unset($hiddenFieldArr['group']);
1441 $markerArray['###SELECTBOX_GROUP_VALUES###'] = $this->renderSelectBoxValues($this->piVars['group'], $optValues['group']);
1442 }
1443 if ($this->conf['blind.']['extResume']) {
1444 $html = $this->cObj->substituteSubpart($html, '###SELECT_EXTRESUME###', '');
1445 } else {
1446 $markerArray['###EXT_RESUME_CHECKED###'] = $this->piVars['extResume'] ? ' checked="checked"' : '';
1447 }
1448 } else {
1449 // Extended search
1450 $html = $this->cObj->substituteSubpart($html, '###SEARCH_FORM_EXTENDED###', '');
1451 }
1452 if ($this->conf['show.']['advancedSearchLink']) {
1453 $linkToOtherMode = $this->piVars['ext'] ? $this->pi_getPageLink($this->frontendController->id, $this->frontendController->sPre) : $this->pi_getPageLink($this->frontendController->id, $this->frontendController->sPre, array($this->prefixId . '[ext]' => 1));
1454 $markerArray['###LINKTOOTHERMODE###'] = '<a href="' . htmlspecialchars($linkToOtherMode) . '">' . $this->pi_getLL(($this->piVars['ext'] ? 'link_regularSearch' : 'link_advancedSearch'), '', true) . '</a>';
1455 } else {
1456 $markerArray['###LINKTOOTHERMODE###'] = '';
1457 }
1458 // Write all hidden fields
1459 $html = $this->cObj->substituteSubpart($html, '###HIDDEN_FIELDS###', implode('', $hiddenFieldArr));
1460 $substitutedContent = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1461 return $substitutedContent;
1462 }
1463
1464 /**
1465 * Function, rendering selector box values.
1466 *
1467 * @param string $value Current value
1468 * @param array $optValues Array with the options as key=>value pairs
1469 * @return string <options> imploded.
1470 */
1471 public function renderSelectBoxValues($value, $optValues)
1472 {
1473 if (!is_array($optValues)) {
1474 return '';
1475 }
1476 $opt = array();
1477 $isSelFlag = 0;
1478 foreach ($optValues as $k => $v) {
1479 $sel = (string)$k === (string)$value ? ' selected="selected"' : '';
1480 if ($sel) {
1481 $isSelFlag++;
1482 }
1483 $opt[] = '<option value="' . htmlspecialchars($k) . '"' . $sel . '>' . htmlspecialchars($v) . '</option>';
1484 }
1485 return implode('', $opt);
1486 }
1487
1488 /**
1489 * Print the searching rules
1490 *
1491 * @return string Rules for the search
1492 */
1493 public function printRules()
1494 {
1495 if (!$this->conf['show.']['rules']) {
1496 return '';
1497 }
1498 $html = $this->cObj->getSubpart($this->templateCode, '###RULES###');
1499 $markerArray['###RULES_HEADER###'] = $this->pi_getLL('rules_header', '', true);
1500 $markerArray['###RULES_TEXT###'] = nl2br(trim($this->pi_getLL('rules_text', '', true)));
1501 $substitutedContent = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1502 return $this->cObj->stdWrap($substitutedContent, $this->conf['rules_stdWrap.']);
1503 }
1504
1505 /**
1506 * Returns the anchor-links to the sections inside the displayed result rows.
1507 *
1508 * @return string
1509 */
1510 public function printResultSectionLinks()
1511 {
1512 if (empty($this->resultSections)) {
1513 return '';
1514 }
1515 $links = array();
1516 $html = $this->cObj->getSubpart($this->templateCode, '###RESULT_SECTION_LINKS###');
1517 $item = $this->cObj->getSubpart($this->templateCode, '###RESULT_SECTION_LINKS_LINK###');
1518 $anchorPrefix = $GLOBALS['TSFE']->baseUrl ? substr(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'), strlen(GeneralUtility::getIndpEnv('TYPO3_SITE_URL'))) : '';
1519 foreach ($this->resultSections as $id => $dat) {
1520 $markerArray = array();
1521 $aBegin = '<a href="' . htmlspecialchars($anchorPrefix . '#anchor_' . md5($id)) . '">';
1522 $aContent = (trim($dat[0]) ? trim($dat[0]) : htmlspecialchars($this->pi_getLL('unnamedSection'))) . ' (' . $dat[1] . ' ' . $this->pi_getLL(($dat[1] > 1 ? 'word_pages' : 'word_page'), '', true) . ')';
1523 $aEnd = '</a>';
1524 $markerArray['###LINK###'] = $aBegin . $aContent . $aEnd;
1525 $links[] = $this->cObj->substituteMarkerArrayCached($item, $markerArray, array(), array());
1526 }
1527 $html = $this->cObj->substituteMarkerArrayCached($html, array('###LINKS###' => implode('', $links)), array(), array());
1528 return '<div' . $this->pi_classParam('sectionlinks') . '>' . $this->cObj->stdWrap($html, $this->conf['sectionlinks_stdWrap.']) . '</div>';
1529 }
1530
1531 /**
1532 * Returns the section header of the search result.
1533 *
1534 * @param string $id ID for the section (used for anchor link)
1535 * @param string $sectionTitleLinked Section title with linked wrapped around
1536 * @param int $countResultRows Number of results in section
1537 * @return string HTML output
1538 */
1539 public function makeSectionHeader($id, $sectionTitleLinked, $countResultRows)
1540 {
1541 $html = $this->cObj->getSubpart($this->templateCode, '###SECTION_HEADER###');
1542 $markerArray['###ANCHOR_URL###'] = 'anchor_' . md5($id);
1543 $markerArray['###SECTION_TITLE###'] = $sectionTitleLinked;
1544 $markerArray['###RESULT_COUNT###'] = $countResultRows;
1545 $markerArray['###RESULT_NAME###'] = $this->pi_getLL('word_page' . ($countResultRows > 1 ? 's' : ''));
1546 $substitutedContent = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1547 return $substitutedContent;
1548 }
1549
1550 /**
1551 * This prints a single result row, including a recursive call for subrows.
1552 *
1553 * @param array $row Search result row
1554 * @param int $headerOnly 1=Display only header (for sub-rows!), 2=nothing at all
1555 * @return string HTML code
1556 */
1557 public function printResultRow($row, $headerOnly = 0)
1558 {
1559 // Get template content:
1560 $tmplContent = $this->prepareResultRowTemplateData($row, $headerOnly);
1561 if ($hookObj = $this->hookRequest('printResultRow')) {
1562 return $hookObj->printResultRow($row, $headerOnly, $tmplContent);
1563 } else {
1564 $html = $this->cObj->getSubpart($this->templateCode, '###RESULT_OUTPUT###');
1565 if (!is_array($row['_sub'])) {
1566 $html = $this->cObj->substituteSubpart($html, '###ROW_SUB###', '');
1567 }
1568 if (!$headerOnly) {
1569 $html = $this->cObj->substituteSubpart($html, '###ROW_SHORT###', '');
1570 } elseif ($headerOnly == 1) {
1571 $html = $this->cObj->substituteSubpart($html, '###ROW_LONG###', '');
1572 } elseif ($headerOnly == 2) {
1573 $html = $this->cObj->substituteSubpart($html, '###ROW_SHORT###', '');
1574 $html = $this->cObj->substituteSubpart($html, '###ROW_LONG###', '');
1575 }
1576 if (is_array($tmplContent)) {
1577 foreach ($tmplContent as $k => $v) {
1578 $markerArray['###' . GeneralUtility::strtoupper($k) . '###'] = $v;
1579 }
1580 }
1581 // Description text
1582 $markerArray['###TEXT_ITEM_SIZE###'] = $this->pi_getLL('res_size', '', true);
1583 $markerArray['###TEXT_ITEM_CRDATE###'] = $this->pi_getLL('res_created', '', true);
1584 $markerArray['###TEXT_ITEM_MTIME###'] = $this->pi_getLL('res_modified', '', true);
1585 $markerArray['###TEXT_ITEM_PATH###'] = $this->pi_getLL('res_path', '', true);
1586 $html = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1587 // If there are subrows (eg. subpages in a PDF-file or if a duplicate page is selected due to user-login (phash_grouping))
1588 if (is_array($row['_sub'])) {
1589 if ($this->multiplePagesType($row['item_type'])) {
1590 $html = str_replace('###TEXT_ROW_SUB###', $this->pi_getLL('res_otherMatching', '', true), $html);
1591 foreach ($row['_sub'] as $subRow) {
1592 $html .= $this->printResultRow($subRow, 1);
1593 }
1594 } else {
1595 $markerArray['###TEXT_ROW_SUB###'] = $this->pi_getLL('res_otherMatching', '', true);
1596 $html = str_replace('###TEXT_ROW_SUB###', $this->pi_getLL('res_otherPageAsWell', '', true), $html);
1597 }
1598 }
1599 return $html;
1600 }
1601 }
1602
1603 /**
1604 * Returns a results browser
1605 *
1606 * @param bool $showResultCount Show result count
1607 * @param string $addString String appended to "displaying results..." notice.
1608 * @param string $addPart String appended after section "displaying results...
1609 * @param string $freeIndexUid List of integers pointing to free indexing configurations to search. -1 represents no filtering, 0 represents TYPO3 pages only, any number above zero is a uid of an indexing configuration!
1610 * @return string HTML output
1611 */
1612 public function pi_list_browseresults($showResultCount = true, $addString = '', $addPart = '', $freeIndexUid = -1)
1613 {
1614 // Initializing variables:
1615 $pointer = (int)$this->piVars['pointer'];
1616 $count = (int)$this->internal['res_count'];
1617 $results_at_a_time = MathUtility::forceIntegerInRange($this->internal['results_at_a_time'], 1, 1000);
1618 $pageCount = (int)ceil($count / $results_at_a_time);
1619
1620 $links = array();
1621 // only show the result browser if more than one page is needed
1622 if ($pageCount > 1) {
1623 $maxPages = MathUtility::forceIntegerInRange($this->internal['maxPages'], 1, $pageCount);
1624
1625 // Make browse-table/links:
1626 if ($pointer > 0) {
1627 // all pages after the 1st one
1628 $links[] = '<li>' . $this->makePointerSelector_link($this->pi_getLL('pi_list_browseresults_prev', '< Previous', true), $pointer - 1, $freeIndexUid) . '</li>';
1629 }
1630 $minPage = $pointer - (int)floor($maxPages / 2);
1631 $maxPage = $minPage + $maxPages - 1;
1632 // Check if the indexes are within the page limits
1633 if ($minPage < 0) {
1634 $maxPage -= $minPage;
1635 $minPage = 0;
1636 } elseif ($maxPage >= $pageCount) {
1637 $minPage -= $maxPage - $pageCount + 1;
1638 $maxPage = $pageCount - 1;
1639 }
1640 $pageLabel = $this->pi_getLL('pi_list_browseresults_page', 'Page', true);
1641 for ($a = $minPage; $a <= $maxPage; $a++) {
1642 $label = trim($pageLabel . ' ' . ($a + 1));
1643 $link = $this->makePointerSelector_link($label, $a, $freeIndexUid);
1644 if ($a === $pointer) {
1645 $links[] = '<li' . $this->pi_classParam('browselist-currentPage') . '><strong>' . $link . '</strong></li>';
1646 } else {
1647 $links[] = '<li>' . $link . '</li>';
1648 }
1649 }
1650 if ($pointer + 1 < $pageCount) {
1651 $links[] = '<li>' . $this->makePointerSelector_link($this->pi_getLL('pi_list_browseresults_next', 'Next >', true), $pointer + 1, $freeIndexUid) . '</li>';
1652 }
1653 }
1654 if (!empty($links)) {
1655 $addPart .= '
1656 <ul class="browsebox">
1657 ' . implode('', $links) . '
1658 </ul>';
1659 }
1660 $label = str_replace(
1661 array('###TAG_BEGIN###', '###TAG_END###'),
1662 array('<strong>', '</strong>'),
1663 $this->pi_getLL('pi_list_browseresults_display', 'Displaying results ###TAG_BEGIN###%1$s to %2$s###TAG_END### out of ###TAG_BEGIN###%3$s###TAG_END###')
1664 );
1665 $resultsFrom = $pointer * $results_at_a_time + 1;
1666 $resultsTo = min($resultsFrom + $results_at_a_time - 1, $count);
1667 $resultCountText = '';
1668 if ($showResultCount) {
1669 $resultCountText = '<p>' . sprintf($label, $resultsFrom, $resultsTo, $count) . $addString . '</p>';
1670 }
1671 $sTables = '<div' . $this->pi_classParam('browsebox') . '>'
1672 . $resultCountText
1673 . $addPart . '</div>';
1674 return $sTables;
1675 }
1676
1677 /***********************************
1678 *
1679 * Support functions for HTML output (with a minimum of fixed markup)
1680 *
1681 ***********************************/
1682 /**
1683 * Preparing template data for the result row output
1684 *
1685 * @param array $row Result row
1686 * @param bool $headerOnly If set, display only header of result (for sub-results)
1687 * @return array Array with data to insert in result row template
1688 */
1689 public function prepareResultRowTemplateData($row, $headerOnly)
1690 {
1691 // Initialize:
1692 $specRowConf = $this->getSpecialConfigForRow($row);
1693 $CSSsuffix = $specRowConf['CSSsuffix'] ? '-' . $specRowConf['CSSsuffix'] : '';
1694 // If external media, link to the media-file instead.
1695 if ($row['item_type']) {
1696 // External media
1697 if ($row['show_resume']) {
1698 // Can link directly.
1699 $targetAttribute = '';
1700 if ($this->frontendController->config['config']['fileTarget']) {
1701 $targetAttribute = ' target="' . htmlspecialchars($this->frontendController->config['config']['fileTarget']) . '"';
1702 }
1703 $title = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($this->makeTitle($row)) . '</a>';
1704 } else {
1705 // Suspicious, so linking to page instead...
1706 $copy_row = $row;
1707 unset($copy_row['cHashParams']);
1708 $title = $this->linkPage($row['page_id'], htmlspecialchars($this->makeTitle($row)), $copy_row);
1709 }
1710 } else {
1711 // Else the page:
1712 // Prepare search words for markup in content:
1713 if ($this->conf['forwardSearchWordsInResultLink']) {
1714 if ($this->conf['forwardSearchWordsInResultLink.']['no_cache']) {
1715 $markUpSwParams = array('no_cache' => 1);
1716 } else {
1717 $markUpSwParams = array();
1718 }
1719 foreach ($this->sWArr as $d) {
1720 $markUpSwParams['sword_list'][] = $d['sword'];
1721 }
1722 } else {
1723 $markUpSwParams = array();
1724 }
1725 $title = $this->linkPage($row['data_page_id'], htmlspecialchars($this->makeTitle($row)), $row, $markUpSwParams);
1726 }
1727 $tmplContent = array();
1728 $tmplContent['title'] = $title;
1729 $tmplContent['result_number'] = $this->conf['show.']['resultNumber'] ? $row['result_number'] . ': ' : '&nbsp;';
1730 $tmplContent['icon'] = $this->makeItemTypeIcon($row['item_type'], '', $specRowConf);
1731 $tmplContent['rating'] = $this->makeRating($row);
1732 $tmplContent['description'] = $this->makeDescription(
1733 $row,
1734 !($this->piVars['extResume'] && !$headerOnly),
1735 $this->conf['results.']['summaryCropAfter']
1736 );
1737 $tmplContent = $this->makeInfo($row, $tmplContent);
1738 $tmplContent['access'] = $this->makeAccessIndication($row['page_id']);
1739 $tmplContent['language'] = $this->makeLanguageIndication($row);
1740 $tmplContent['CSSsuffix'] = $CSSsuffix;
1741 // Post processing with hook.
1742 if ($hookObj = $this->hookRequest('prepareResultRowTemplateData_postProc')) {
1743 $tmplContent = $hookObj->prepareResultRowTemplateData_postProc($tmplContent, $row, $headerOnly);
1744 }
1745 return $tmplContent;
1746 }
1747
1748 /**
1749 * Returns a string that tells which search words are searched for.
1750 *
1751 * @param array Array of search words
1752 * @return string HTML telling what is searched for.
1753 */
1754 public function tellUsWhatIsSeachedFor($sWArr)
1755 {
1756 // Init:
1757 $searchingFor = '';
1758 $c = 0;
1759 // Traverse search words:
1760 foreach ($sWArr as $k => $v) {
1761 if ($c) {
1762 switch ($v['oper']) {
1763 case 'OR':
1764 $searchingFor .= ' ' . $this->pi_getLL('searchFor_or', '', true) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1765 break;
1766 case 'AND NOT':
1767 $searchingFor .= ' ' . $this->pi_getLL('searchFor_butNot', '', true) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1768 break;
1769 default:
1770 // AND...
1771 $searchingFor .= ' ' . $this->pi_getLL('searchFor_and', '', true) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1772 }
1773 } else {
1774 $searchingFor = $this->pi_getLL('searchFor', '', true) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1775 }
1776 $c++;
1777 }
1778 return $searchingFor;
1779 }
1780
1781 /**
1782 * Wraps the search words in the search-word list display (from ->tellUsWhatIsSeachedFor())
1783 *
1784 * @param string $str search word to wrap (in local charset!)
1785 * @return string Search word wrapped in <span> tag.
1786 */
1787 public function wrapSW($str)
1788 {
1789 return '"<span' . $this->pi_classParam('sw') . '>' . htmlspecialchars($str) . '</span>"';
1790 }
1791
1792 /**
1793 * Makes a selector box
1794 *
1795 * @param string $name Name of selector box
1796 * @param string $value Current value
1797 * @param array $optValues Array of options in the selector box (value => label pairs)
1798 * @return string HTML of selector box
1799 */
1800 public function renderSelectBox($name, $value, $optValues)
1801 {
1802 if (is_array($optValues)) {
1803 $opt = array();
1804 $isSelFlag = 0;
1805 foreach ($optValues as $k => $v) {
1806 $sel = (string)$k === (string)$value ? ' selected="selected"' : '';
1807 if ($sel) {
1808 $isSelFlag++;
1809 }
1810 $opt[] = '<option value="' . htmlspecialchars($k) . '"' . $sel . '>' . htmlspecialchars($v) . '</option>';
1811 }
1812 return '<select name="' . $name . '">' . implode('', $opt) . '</select>';
1813 }
1814 }
1815
1816 /**
1817 * Used to make the link for the result-browser.
1818 * Notice how the links must resubmit the form after setting the new pointer-value in a hidden formfield.
1819 *
1820 * @param string $str String to wrap in <a> tag
1821 * @param int $p Pointer value
1822 * @param string $freeIndexUid List of integers pointing to free indexing configurations to search. -1 represents no filtering, 0 represents TYPO3 pages only, any number above zero is a uid of an indexing configuration!
1823 * @return string Input string wrapped in <a> tag with onclick event attribute set.
1824 */
1825 public function makePointerSelector_link($str, $p, $freeIndexUid)
1826 {
1827 $onclick = 'document.getElementById(' . GeneralUtility::quoteJSvalue($this->prefixId . '_pointer') . ').value=' . GeneralUtility::quoteJSvalue($p) . ';' . 'document.getElementById(' . GeneralUtility::quoteJSvalue($this->prefixId . '_freeIndexUid') . ').value=' . GeneralUtility::quoteJSvalue($freeIndexUid) . ';' . 'document.getElementById(' . GeneralUtility::quoteJSvalue($this->prefixId) . ').submit();return false;';
1828 return '<a href="#" onclick="' . htmlspecialchars($onclick) . '">' . $str . '</a>';
1829 }
1830
1831 /**
1832 * Return icon for file extension
1833 *
1834 * @param string $it File extension / item type
1835 * @param string $alt Title attribute value in icon.
1836 * @param array $specRowConf TypoScript configuration specifically for search result.
1837 * @return string <img> tag for icon
1838 */
1839 public function makeItemTypeIcon($it, $alt = '', $specRowConf)
1840 {
1841 // Build compound key if item type is 0, iconRendering is not used
1842 // and specConfs.[pid].pageIcon was set in TS
1843 if ($it === '0' && $specRowConf['_pid'] && is_array($specRowConf['pageIcon.']) && !is_array($this->conf['iconRendering.'])) {
1844 $it .= ':' . $specRowConf['_pid'];
1845 }
1846 if (!isset($this->iconFileNameCache[$it])) {
1847 $this->iconFileNameCache[$it] = '';
1848 // If TypoScript is used to render the icon:
1849 if (is_array($this->conf['iconRendering.'])) {
1850 $this->cObj->setCurrentVal($it);
1851 $this->iconFileNameCache[$it] = $this->cObj->cObjGetSingle($this->conf['iconRendering'], $this->conf['iconRendering.']);
1852 } else {
1853 // Default creation / finding of icon:
1854 $icon = '';
1855 if ($it === '0' || substr($it, 0, 2) == '0:') {
1856 if (is_array($specRowConf['pageIcon.'])) {
1857 $this->iconFileNameCache[$it] = $this->cObj->cObjGetSingle('IMAGE', $specRowConf['pageIcon.']);
1858 } else {
1859 $icon = 'EXT:indexed_search/Resources/Public/Icons/FileTypes/pages.gif';
1860 }
1861 } elseif ($this->external_parsers[$it]) {
1862 $icon = $this->external_parsers[$it]->getIcon($it);
1863 }
1864 if ($icon) {
1865 $fullPath = GeneralUtility::getFileAbsFileName($icon);
1866 if ($fullPath) {
1867 $info = @getimagesize($fullPath);
1868 $iconPath = \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix($fullPath);
1869 $this->iconFileNameCache[$it] = is_array($info) ? '<img src="' . $iconPath . '" ' . $info[3] . ' title="' . htmlspecialchars($alt) . '" alt="" />' : '';
1870 }
1871 }
1872 }
1873 }
1874 return $this->iconFileNameCache[$it];
1875 }
1876
1877 /**
1878 * Return the rating-HTML code for the result row. This makes use of the $this->firstRow
1879 *
1880 * @param array $row Result row array
1881 * @return string String showing ranking value
1882 */
1883 public function makeRating($row)
1884 {
1885 switch ((string)$this->piVars['order']) {
1886 case 'rank_count':
1887 // Number of occurencies on page
1888 return $row['order_val'] . ' ' . $this->pi_getLL('maketitle_matches');
1889 break;
1890 case 'rank_first':
1891 // Close to top of page
1892 return ceil(MathUtility::forceIntegerInRange((255 - $row['order_val']), 1, 255) / 255 * 100) . '%';
1893 break;
1894 case 'rank_flag':
1895 // Based on priority assigned to <title> / <meta-keywords> / <meta-description> / <body>
1896 if ($this->firstRow['order_val2']) {
1897 $base = $row['order_val1'] * 256;
1898 // (3 MSB bit, 224 is highest value of order_val1 currently)
1899 $freqNumber = $row['order_val2'] / $this->firstRow['order_val2'] * pow(2, 12);
1900 // 15-3 MSB = 12
1901 $total = MathUtility::forceIntegerInRange($base + $freqNumber, 0, 32767);
1902 return ceil(log($total) / log(32767) * 100) . '%';
1903 }
1904 break;
1905 case 'rank_freq':
1906 // Based on frequency
1907 $max = 10000;
1908 $total = MathUtility::forceIntegerInRange($row['order_val'], 0, $max);
1909 return ceil(log($total) / log($max) * 100) . '%';
1910 break;
1911 case 'crdate':
1912 // Based on creation date
1913 return $this->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_crdate'], 0);
1914 break;
1915 case 'mtime':
1916 // Based on modification time
1917 return $this->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_mtime'], 0);
1918 break;
1919 default:
1920 // fx. title
1921 return '&nbsp;';
1922 }
1923 }
1924
1925 /**
1926 * Returns the resume for the search-result.
1927 *
1928 * @param array $row Search result row
1929 * @param bool $noMarkup If noMarkup is FALSE, then the index_fulltext table is used to select the content of the page, split it with regex to display the search words in the text.
1930 * @param int $lgd String length
1931 * @return string HTML string
1932 */
1933 public function makeDescription($row, $noMarkup = false, $lgd = 180)
1934 {
1935 if ($row['show_resume']) {
1936 $markedSW = '';
1937 $outputStr = '';
1938 if (!$noMarkup) {
1939 if (\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_fulltext')) {
1940 $res = $this->databaseConnection->exec_SELECTquery('*', 'index_fulltext', 'phash=' . (int)$row['phash']);
1941 } else {
1942 $res = false;
1943 }
1944 if ($res) {
1945 if ($ftdrow = $this->databaseConnection->sql_fetch_assoc($res)) {
1946 // Cut HTTP references after some length
1947 $content = preg_replace('/(http:\\/\\/[^ ]{' . $this->conf['results.']['hrefInSummaryCropAfter'] . '})([^ ]+)/i', '$1...', $ftdrow['fulltextdata']);
1948 $markedSW = $this->markupSWpartsOfString($content);
1949 }
1950 $this->databaseConnection->sql_free_result($res);
1951 }
1952 }
1953 if (!trim($markedSW)) {
1954 $outputStr = $this->charsetConverter->crop('utf-8', $row['item_description'], $lgd, $this->conf['results.']['summaryCropSignifier']);
1955 $outputStr = htmlspecialchars($outputStr);
1956 }
1957 $output = $this->utf8_to_currentCharset($outputStr ?: $markedSW);
1958 } else {
1959 $output = '<span class="noResume">' . $this->pi_getLL('res_noResume', '', true) . '</span>';
1960 }
1961 return $output;
1962 }
1963
1964 /**
1965 * Marks up the search words from $this->sWarr in the $str with a color.
1966 *
1967 * @param string $str Text in which to find and mark up search words. This text is assumed to be UTF-8 like the search words internally is.
1968 * @return string Processed content.
1969 */
1970 public function markupSWpartsOfString($str)
1971 {
1972 $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
1973 // Init:
1974 $str = str_replace('&nbsp;', ' ', $htmlParser->bidir_htmlspecialchars($str, -1));
1975 $str = preg_replace('/\\s\\s+/', ' ', $str);
1976 $swForReg = array();
1977 // Prepare search words for regex:
1978 foreach ($this->sWArr as $d) {
1979 $swForReg[] = preg_quote($d['sword'], '/');
1980 }
1981 $regExString = '(' . implode('|', $swForReg) . ')';
1982 // Split and combine:
1983 $parts = preg_split('/' . $regExString . '/ui', ' ' . $str . ' ', 20000, PREG_SPLIT_DELIM_CAPTURE);
1984 // Constants:
1985 $summaryMax = $this->conf['results.']['markupSW_summaryMax'];
1986 $postPreLgd = $this->conf['results.']['markupSW_postPreLgd'];
1987 $postPreLgd_offset = $this->conf['results.']['markupSW_postPreLgd_offset'];
1988 $divider = $this->conf['results.']['markupSW_divider'];
1989 $occurencies = (count($parts) - 1) / 2;
1990 if ($occurencies) {
1991 $postPreLgd = MathUtility::forceIntegerInRange($summaryMax / $occurencies, $postPreLgd, $summaryMax / 2);
1992 }
1993 // Variable:
1994 $summaryLgd = 0;
1995 $output = array();
1996 // Shorten in-between strings:
1997 foreach ($parts as $k => $strP) {
1998 if ($k % 2 == 0) {
1999 // Find length of the summary part:
2000 $strLen = $this->charsetConverter->strlen('utf-8', $parts[$k]);
2001 $output[$k] = $parts[$k];
2002 // Possibly shorten string:
2003 if (!$k) {
2004 // First entry at all (only cropped on the frontside)
2005 if ($strLen > $postPreLgd) {
2006 $output[$k] = $divider . preg_replace('/^[^[:space:]]+[[:space:]]/', '', $this->charsetConverter->crop('utf-8', $parts[$k], -($postPreLgd - $postPreLgd_offset)));
2007 }
2008 } elseif ($summaryLgd > $summaryMax || !isset($parts[$k + 1])) {
2009 // In case summary length is exceed OR if there are no more entries at all:
2010 if ($strLen > $postPreLgd) {
2011 $output[$k] = preg_replace('/[[:space:]][^[:space:]]+$/', '', $this->charsetConverter->crop('utf-8', $parts[$k], ($postPreLgd - $postPreLgd_offset))) . $divider;
2012 }
2013 } else {
2014 // In-between search words:
2015 if ($strLen > $postPreLgd * 2) {
2016 $output[$k] = preg_replace('/[[:space:]][^[:space:]]+$/', '', $this->charsetConverter->crop('utf-8', $parts[$k], ($postPreLgd - $postPreLgd_offset))) . $divider . preg_replace('/^[^[:space:]]+[[:space:]]/', '', $this->charsetConverter->crop('utf-8', $parts[$k], -($postPreLgd - $postPreLgd_offset)));
2017 }
2018 }
2019 $summaryLgd += $this->charsetConverter->strlen('utf-8', $output[$k]);
2020 // Protect output:
2021 $output[$k] = htmlspecialchars($output[$k]);
2022 // If summary lgd is exceed, break the process:
2023 if ($summaryLgd > $summaryMax) {
2024 break;
2025 }
2026 } else {
2027 $summaryLgd += $this->charsetConverter->strlen('utf-8', $strP);
2028 $output[$k] = '<strong class="tx-indexedsearch-redMarkup">' . htmlspecialchars($parts[$k]) . '</strong>';
2029 }
2030 }
2031 // Return result:
2032 return implode('', $output);
2033 }
2034
2035 /**
2036 * Returns the title of the search result row
2037 *
2038 * @param array $row Result row
2039 * @return string Title from row
2040 */
2041 public function makeTitle($row)
2042 {
2043 $add = '';
2044 if ($this->multiplePagesType($row['item_type'])) {
2045 $dat = unserialize($row['cHashParams']);
2046 $pp = explode('-', $dat['key']);
2047 if ($pp[0] != $pp[1]) {
2048 $add = ', ' . $this->pi_getLL('word_pages') . ' ' . $dat['key'];
2049 } else {
2050 $add = ', ' . $this->pi_getLL('word_page') . ' ' . $pp[0];
2051 }
2052 }
2053 $outputString = $this->charsetConverter->crop('utf-8', $row['item_title'], $this->conf['results.']['titleCropAfter'], $this->conf['results.']['titleCropSignifier']);
2054 return $this->utf8_to_currentCharset($outputString) . $add;
2055 }
2056
2057 /**
2058 * Returns the info-string in the bottom of the result-row display (size, dates, path)
2059 *
2060 * @param array $row Result row
2061 * @param array $tmplArray Template array to modify
2062 * @return array Modified template array
2063 */
2064 public function makeInfo($row, $tmplArray)
2065 {
2066 $tmplArray['size'] = GeneralUtility::formatSize($row['item_size']);
2067 $tmplArray['created'] = $this->formatCreatedDate($row['item_crdate']);
2068 $tmplArray['modified'] = $this->formatModifiedDate($row['item_mtime']);
2069 $pathId = $row['data_page_id'] ?: $row['page_id'];
2070 $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
2071 $pI = parse_url($row['data_filename']);
2072 if ($pI['scheme']) {
2073 $targetAttribute = '';
2074 if ($this->frontendController->config['config']['fileTarget']) {
2075 $targetAttribute = ' target="' . htmlspecialchars($this->frontendController->config['config']['fileTarget']) . '"';
2076 }
2077 $tmplArray['path'] = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($row['data_filename']) . '</a>';
2078 } else {
2079 $pathStr = $this->getPathFromPageId($pathId, $pathMP);
2080 $tmplArray['path'] = $this->linkPage($pathId, $pathStr, array(
2081 'cHashParams' => $row['cHashParams'],
2082 'data_page_type' => $row['data_page_type'],
2083 'data_page_mp' => $pathMP,
2084 'sys_language_uid' => $row['sys_language_uid']
2085 ));
2086 }
2087 return $tmplArray;
2088 }
2089
2090 /**
2091 * Returns configuration from TypoScript for result row based on ID / location in page tree!
2092 *
2093 * @param array $row Result row
2094 * @return array Configuration array
2095 */
2096 public function getSpecialConfigForRow($row)
2097 {
2098 $pathId = $row['data_page_id'] ?: $row['page_id'];
2099 $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
2100 $rl = $this->getRootLine($pathId, $pathMP);
2101 $specConf = $this->conf['specConfs.']['0.'];
2102 if (is_array($rl)) {
2103 foreach ($rl as $dat) {
2104 if (is_array($this->conf['specConfs.'][$dat['uid'] . '.'])) {
2105 $specConf = $this->conf['specConfs.'][$dat['uid'] . '.'];
2106 $specConf['_pid'] = $dat['uid'];
2107 break;
2108 }
2109 }
2110 }
2111 return $specConf;
2112 }
2113
2114 /**
2115 * Returns the HTML code for language indication.
2116 *
2117 * @param array $row Result row
2118 * @return string HTML code for result row.
2119 */
2120 public function makeLanguageIndication($row)
2121 {
2122 // If search result is a TYPO3 page:
2123 if ((string)$row['item_type'] === '0') {
2124 // If TypoScript is used to render the flag:
2125 if (is_array($this->conf['flagRendering.'])) {
2126 $this->cObj->setCurrentVal($row['sys_language_uid']);
2127 return $this->cObj->cObjGetSingle($this->conf['flagRendering'], $this->conf['flagRendering.']);
2128 }
2129 }
2130 return '&nbsp;';
2131 }
2132
2133 /**
2134 * Returns the HTML code for the locking symbol.
2135 * NOTICE: Requires a call to ->getPathFromPageId() first in order to work (done in ->makeInfo() by calling that first)
2136 *
2137 * @param int $id Page id for which to find answer
2138 * @return string <img> tag if access is limited.
2139 */
2140 public function makeAccessIndication($id)
2141 {
2142 if (is_array($this->fe_groups_required[$id]) && !empty($this->fe_groups_required[$id])) {
2143 return '<img src="' . \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::siteRelPath('indexed_search') . 'Resources/Public/Icons/FileTypes/locked.gif" width="12" height="15" vspace="5" title="' . sprintf($this->pi_getLL('res_memberGroups', '', true), implode(',', array_unique($this->fe_groups_required[$id]))) . '" alt="" />';
2144 }
2145
2146 return '';
2147 }
2148
2149 /**
2150 * Links the $str to page $id
2151 *
2152 * @param int $id Page id
2153 * @param string $str Title String to link
2154 * @param array $row Result row
2155 * @param array $markUpSwParams Additional parameters for marking up seach words
2156 * @return string <A> tag wrapped title string.
2157 */
2158 public function linkPage($id, $str, $row = array(), $markUpSwParams = array())
2159 {
2160 // Parameters for link:
2161 $urlParameters = (array)unserialize($row['cHashParams']);
2162 // Add &type and &MP variable:
2163 if ($row['data_page_type']) {
2164 $urlParameters['type'] = $row['data_page_type'];
2165 }
2166 if ($row['data_page_mp']) {
2167 $urlParameters['MP'] = $row['data_page_mp'];
2168 }
2169 if ($row['sys_language_uid']) {
2170 $urlParameters['L'] = $row['sys_language_uid'];
2171 }
2172 // markup-GET vars:
2173 $urlParameters = array_merge($urlParameters, $markUpSwParams);
2174 // This will make sure that the path is retrieved if it hasn't been already. Used only for the sake of the domain_record thing...
2175 if (!is_array($this->domain_records[$id])) {
2176 $this->getPathFromPageId($id);
2177 }
2178 // If external domain, then link to that:
2179 if (!empty($this->domain_records[$id])) {
2180 reset($this->domain_records[$id]);
2181 $firstDom = current($this->domain_records[$id]);
2182 $scheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://';
2183 $addParams = '';
2184 if (is_array($urlParameters) && !empty($urlParameters)) {
2185 $addParams .= GeneralUtility::implodeArrayForUrl('', $urlParameters);
2186 }
2187 if ($target = $this->conf['search.']['detect_sys_domain_records.']['target']) {
2188 $target = ' target="' . $target . '"';
2189 }
2190 return '<a href="' . htmlspecialchars(($scheme . $firstDom . '/index.php?id=' . $id . $addParams)) . '"' . $target . '>' . htmlspecialchars($str) . '</a>';
2191 } else {
2192 return $this->pi_linkToPage($str, $id, $this->conf['result_link_target'], $urlParameters);
2193 }
2194 }
2195
2196 /**
2197 * Returns the path to the page $id
2198 *
2199 * @param int $id Page ID
2200 * @param string $pathMP MP variable content.
2201 * @return string Root line for result.
2202 */
2203 public function getRootLine($id, $pathMP = '')
2204 {
2205 $identStr = $id . '|' . $pathMP;
2206 if (!isset($this->cache_path[$identStr])) {
2207 $this->cache_rl[$identStr] = $this->frontendController->sys_page->getRootLine($id, $pathMP);
2208 }
2209 return $this->cache_rl[$identStr];
2210 }
2211
2212 /**
2213 * Gets the first sys_domain record for the page, $id
2214 *
2215 * @param int $id Page id
2216 * @return string Domain name
2217 */
2218 public function getFirstSysDomainRecordForPage($id)
2219 {
2220 $res = $this->databaseConnection->exec_SELECTquery('domainName', 'sys_domain', 'pid=' . (int)$id . $this->cObj->enableFields('sys_domain'), '', 'sorting');
2221 $row = $this->databaseConnection->sql_fetch_assoc($res);
2222 return rtrim($row['domainName'], '/');
2223 }
2224
2225 /**
2226 * Returns the path to the page $id
2227 *
2228 * @param int $id Page ID
2229 * @param string $pathMP MP variable content
2230 * @return string Path (HTML-escaped)
2231 */
2232 public function getPathFromPageId($id, $pathMP = '')
2233 {
2234 $identStr = $id . '|' . $pathMP;
2235 if (!isset($this->cache_path[$identStr])) {
2236 $this->fe_groups_required[$id] = array();
2237 $this->domain_records[$id] = array();
2238 $rl = $this->getRootLine($id, $pathMP);
2239 $path = '';
2240 $pageCount = count($rl);
2241 if (is_array($rl) && !empty($rl)) {
2242 $index = 0;
2243 $breadcrumbWrap = isset($this->conf['breadcrumbWrap']) ? $this->conf['breadcrumbWrap'] : '/';
2244 $breadcrumbWraps = $GLOBALS['TSFE']->tmpl->splitConfArray(array('wrap' => $breadcrumbWrap), $pageCount);
2245 foreach ($rl as $k => $v) {
2246 // Check fe_user
2247 if ($v['fe_group'] && ($v['uid'] == $id || $v['extendToSubpages'])) {
2248 $this->fe_groups_required[$id][] = $v['fe_group'];
2249 }
2250 // Check sys_domain.
2251 if ($this->conf['search.']['detect_sys_domain_records']) {
2252 $sysDName = $this->getFirstSysDomainRecordForPage($v['uid']);
2253 if ($sysDName) {
2254 $this->domain_records[$id][] = $sysDName;
2255 // Set path accordingly:
2256 $path = $sysDName . $path;
2257 break;
2258 }
2259 }
2260 // Stop, if we find that the current id is the current root page.
2261 if ($v['uid'] == $this->frontendController->config['rootLine'][0]['uid']) {
2262 array_pop($breadcrumbWraps);
2263 break;
2264 }
2265 $path = $this->cObj->wrap(htmlspecialchars($v['title']), array_pop($breadcrumbWraps)['wrap']) . $path;
2266 }
2267 }
2268 $this->cache_path[$identStr] = $path;
2269 if (is_array($this->conf['path_stdWrap.'])) {
2270 $this->cache_path[$identStr] = $this->cObj->stdWrap($this->cache_path[$identStr], $this->conf['path_stdWrap.']);
2271 }
2272 }
2273 return $this->cache_path[$identStr];
2274 }
2275
2276 /**
2277 * Return the menu of pages used for the selector.
2278 *
2279 * @param int $id Page ID for which to return menu
2280 * @return array Menu items (for making the section selector box)
2281 */
2282 public function getMenu($id)
2283 {
2284 if ($this->conf['show.']['LxALLtypes']) {
2285 $output = array();
2286 $res = $this->databaseConnection->exec_SELECTquery('title,uid', 'pages', 'pid=' . (int)$id . $this->cObj->enableFields('pages'), '', 'sorting');
2287 while ($row = $this->databaseConnection->sql_fetch_assoc($res)) {
2288 $output[$row['uid']] = $this->frontendController->sys_page->getPageOverlay($row);
2289 }
2290 $this->databaseConnection->sql_free_result($res);
2291 return $output;
2292 } else {
2293 return $this->frontendController->sys_page->getMenu($id);
2294 }
2295 }
2296
2297 /**
2298 * Returns if an item type is a multipage item type
2299 *
2300 * @param string $item_type Item type
2301 * @return bool TRUE if multipage capable
2302 */
2303 public function multiplePagesType($item_type)
2304 {
2305 return is_object($this->external_parsers[$item_type]) && $this->external_parsers[$item_type]->isMultiplePageExtension($item_type);
2306 }
2307
2308 /**
2309 * Converts the input string from utf-8 to the backend charset.
2310 *
2311 * @param string String to convert (utf-8)
2312 * @return string Converted string (backend charset if different from utf-8)
2313 */
2314 public function utf8_to_currentCharset($str)
2315 {
2316 return $this->frontendController->csConv($str, 'utf-8');
2317 }
2318
2319 /**
2320 * Returns an object reference to the hook object if any
2321 *
2322 * @param string $functionName Name of the function you want to call / hook key
2323 * @return object|NULL Hook object, if any. Otherwise NULL.
2324 */
2325 public function hookRequest($functionName)
2326 {
2327 // Hook: menuConfig_preProcessModMenu
2328 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
2329 $hookObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
2330 if (method_exists($hookObj, $functionName)) {
2331 $hookObj->pObj = $this;
2332 return $hookObj;
2333 }
2334 }
2335 }
2336
2337 /**
2338 * Obtains the URL of the search target page
2339 *
2340 * @return string
2341 */
2342 protected function getSearchFormActionURL()
2343 {
2344 $targetUrlPid = $this->getSearchFormActionPidFromTS();
2345 if ($targetUrlPid == 0) {
2346 $targetUrlPid = $this->frontendController->id;
2347 }
2348 return $this->pi_getPageLink($targetUrlPid, $this->frontendController->sPre);
2349 }
2350
2351 /**
2352 * Obtains search form target pid from the TypoScript configuration
2353 *
2354 * @return int
2355 */
2356 protected function getSearchFormActionPidFromTS()
2357 {
2358 $result = 0;
2359 if (isset($this->conf['search.']['targetPid']) || isset($this->conf['search.']['targetPid.'])) {
2360 if (is_array($this->conf['search.']['targetPid.'])) {
2361 $result = $this->cObj->stdWrap($this->conf['search.']['targetPid'], $this->conf['search.']['targetPid.']);
2362 } else {
2363 $result = $this->conf['search.']['targetPid'];
2364 }
2365 $result = (int)$result;
2366 }
2367 return $result;
2368 }
2369
2370 /**
2371 * Formats date as 'created' date
2372 *
2373 * @param int $date
2374 * @return string
2375 */
2376 protected function formatCreatedDate($date)
2377 {
2378 $defaultFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
2379 return $this->formatDate($date, 'created', $defaultFormat);
2380 }
2381
2382 /**
2383 * Formats date as 'modified' date
2384 *
2385 * @param int $date
2386 * @return string
2387 */
2388 protected function formatModifiedDate($date)
2389 {
2390 $defaultFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
2391 return $this->formatDate($date, 'modified', $defaultFormat);
2392 }
2393
2394 /**
2395 * Formats the date using format string from TypoScript or default format
2396 * if TypoScript format is not set
2397 *
2398 * @param int $date
2399 * @param string $tsKey
2400 * @param string $defaultFormat
2401 * @return string
2402 */
2403 protected function formatDate($date, $tsKey, $defaultFormat)
2404 {
2405 $strftimeFormat = $this->conf['dateFormat.'][$tsKey];
2406 if ($strftimeFormat) {
2407 $result = strftime($strftimeFormat, $date);
2408 } else {
2409 $result = date($defaultFormat, $date);
2410 }
2411 return $result;
2412 }
2413
2414 /**
2415 * Search type
2416 * e.g. sentence (20), any part of the word (1)
2417 *
2418 * @return int
2419 */
2420 public function getSearchType()
2421 {
2422 return (int)$this->piVars['type'];
2423 }
2424
2425 /**
2426 * A list of integer which should be root-pages to search from
2427 *
2428 * @return int[]
2429 */
2430 public function getSearchRootPageIdList()
2431 {
2432 return \TYPO3\CMS\Core\Utility\GeneralUtility::intExplode(',', $this->wholeSiteIdList);
2433 }
2434
2435 /**
2436 * Getter for join_pages flag
2437 * enabled through $this->conf['search.']['skipExtendToSubpagesChecking']
2438 *
2439 * @return bool
2440 */
2441 public function getJoinPagesForQuery()
2442 {
2443 return (bool)$this->join_pages;
2444 }
2445
2446 /**
2447 * Load settings and apply stdWrap to them
2448 */
2449 protected function loadSettings()
2450 {
2451 if (!is_array($this->conf['results.'])) {
2452 $this->conf['results.'] = array();
2453 }
2454 $this->conf['results.']['summaryCropAfter'] = MathUtility::forceIntegerInRange(
2455 $this->cObj->stdWrap($this->conf['results.']['summaryCropAfter'], $this->conf['results.']['summaryCropAfter.']),
2456 10, 5000, 180
2457 );
2458 $this->conf['results.']['summaryCropSignifier'] = $this->cObj->stdWrap($this->conf['results.']['summaryCropSignifier'], $this->conf['results.']['summaryCropSignifier.']);
2459 $this->conf['results.']['titleCropAfter'] = MathUtility::forceIntegerInRange(
2460 $this->cObj->stdWrap($this->conf['results.']['titleCropAfter'], $this->conf['results.']['titleCropAfter.']),
2461 10, 500, 50
2462 );
2463 $this->conf['results.']['titleCropSignifier'] = $this->cObj->stdWrap($this->conf['results.']['titleCropSignifier'], $this->conf['results.']['titleCropSignifier.']);
2464 $this->conf['results.']['markupSW_summaryMax'] = MathUtility::forceIntegerInRange(
2465 $this->cObj->stdWrap($this->conf['results.']['markupSW_summaryMax'], $this->conf['results.']['markupSW_summaryMax.']),
2466 10, 5000, 300
2467 );
2468 $this->conf['results.']['markupSW_postPreLgd'] = MathUtility::forceIntegerInRange(
2469 $this->cObj->stdWrap($this->conf['results.']['markupSW_postPreLgd'], $this->conf['results.']['markupSW_postPreLgd.']),
2470 1, 500, 60
2471 );
2472 $this->conf['results.']['markupSW_postPreLgd_offset'] = MathUtility::forceIntegerInRange(
2473 $this->cObj->stdWrap($this->conf['results.']['markupSW_postPreLgd_offset'], $this->conf['results.']['markupSW_postPreLgd_offset.']),
2474 1, 50, 5
2475 );
2476 $this->conf['results.']['markupSW_divider'] = $this->cObj->stdWrap($this->conf['results.']['markupSW_divider'], $this->conf['results.']['markupSW_divider.']);
2477 $this->conf['results.']['hrefInSummaryCropAfter'] = MathUtility::forceIntegerInRange(
2478 $this->cObj->stdWrap($this->conf['results.']['hrefInSummaryCropAfter'], $this->conf['results.']['hrefInSummaryCropAfter.']),
2479 10, 400, 60
2480 );
2481 $this->conf['results.']['hrefInSummaryCropSignifier'] = $this->cObj->stdWrap($this->conf['results.']['hrefInSummaryCropSignifier'], $this->conf['results.']['hrefInSummaryCropSignifier.']);
2482 }
2483 }