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