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