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