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