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