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