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