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