* The indexed-search lexer has been updated so it supports "printjoins" (characters...
[Packages/TYPO3.CMS.git] / typo3 / sysext / indexed_search / pi / class.tx_indexedsearch.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2001-2004 Kasper Skaarhoj (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 * $Id$
31 *
32 * Creates a searchform for indexed search. Indexing must be enabled
33 * for this to make sense.
34 *
35 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
36 * @co-author Christian Jul Jensen <christian@typo3.com>
37 */
38 /**
39 * [CLASS/FUNCTION INDEX of SCRIPT]
40 *
41 *
42 *
43 * 120: class tx_indexedsearch extends tslib_pibase
44 * 164: function main($content, $conf)
45 * 197: function initialize()
46 * 374: function getSearchWords($defOp)
47 *
48 * SECTION: Main functions
49 * 418: function doSearch($sWArr)
50 * 451: function getResultRows($sWArr)
51 * 521: function getResultRows_SQLpointer($sWArr)
52 * 541: function getDisplayResults($sWArr, $resData)
53 * 592: function compileResult($resultRows)
54 *
55 * SECTION: Searching functions (SQL)
56 * 685: function getPhashList($sWArr)
57 * 786: function execPHashListQuery($wordSel,$plusQ='')
58 * 806: function sectionTableWhere()
59 * 853: function mediaTypeWhere()
60 * 878: function languageWhere()
61 * 890: function execFinalQuery($list)
62 * 1028: function checkResume($row)
63 * 1075: function isDescending($inverse=FALSE)
64 * 1089: function writeSearchStat($sWArr,$count,$pt)
65 *
66 * SECTION: HTML output functions
67 * 1140: function makeSearchForm($optValues)
68 * 1212: function printRules()
69 * 1228: function printResultSectionLinks()
70 * 1250: function makeSectionHeader($id,$sectionTitleLinked,$countResultRows)
71 * 1266: function printResultRow($row, $headerOnly=0)
72 * 1341: function pi_list_browseresults($showResultCount=1,$addString='',$addPart='')
73 *
74 * SECTION: Support functions for HTML output (with a minimum of fixed markup)
75 * 1404: function prepareResultRowTemplateData($row, $headerOnly)
76 * 1448: function tellUsWhatIsSeachedFor($sWArr)
77 * 1482: function wrapSW($str)
78 * 1494: function renderSelectBox($name,$value,$optValues)
79 * 1517: function makePointerSelector_link($str,$p)
80 * 1530: function makeItemTypeIcon($it,$alt='',$specRowConf)
81 * 1572: function makeRating($row)
82 * 1617: function makeDescription($row,$noMarkup=0,$lgd=180)
83 * 1647: function markupSWpartsOfString($str)
84 * 1742: function makeTitle($row)
85 * 1766: function makeInfo($row,$tmplArray)
86 * 1791: function getSpecialConfigForRow($row)
87 * 1815: function makeLanguageIndication($row)
88 * 1852: function makeAccessIndication($id)
89 * 1866: function linkPage($id,$str,$row=array())
90 * 1909: function getRootLine($id,$pathMP='')
91 * 1924: function getFirstSysDomainRecordForPage($id)
92 * 1937: function getPathFromPageId($id,$pathMP='')
93 * 1989: function getMenu($id)
94 * 2008: function multiplePagesType($item_type)
95 * 2018: function utf8_to_currentCharset($str)
96 * 2028: function &hookRequest($functionName)
97 *
98 * TOTAL FUNCTIONS: 45
99 * (This index is automatically created/updated by the extension "extdeveval")
100 *
101 */
102
103
104
105 require_once(PATH_tslib.'class.tslib_pibase.php');
106 require_once(PATH_tslib.'class.tslib_search.php');
107 require_once(t3lib_extMgm::extPath('indexed_search').'class.indexer.php');
108
109
110 /**
111 * Index search frontend
112 *
113 * Creates a searchform for indexed search. Indexing must be enabled
114 * for this to make sense.
115 *
116 * @package TYPO3
117 * @subpackage tx_indexedsearch
118 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
119 */
120 class tx_indexedsearch extends tslib_pibase {
121 var $prefixId = 'tx_indexedsearch'; // Same as class name
122 var $scriptRelPath = 'pi/class.tx_indexedsearch.php'; // Path to this script relative to the extension dir.
123 var $extKey = 'indexed_search'; // The extension key.
124
125 var $join_pages = 0; // See document for info about this flag...
126 var $defaultResultNumber = 20;
127
128 var $operator_translate_table = Array ( // case-sensitive. Defines the words, which will be operators between words
129 Array ('+' , 'AND'),
130 Array ('|' , 'OR'),
131 Array ('-' , 'AND NOT'),
132 // english
133 # Array ('AND' , 'AND'),
134 # Array ('OR' , 'OR'),
135 # Array ('NOT' , 'AND NOT'),
136 );
137
138 // Internal variable
139 var $wholeSiteIdList = 0; // Root-page PIDs to search in (rl0 field where clause, see initialize() function)
140
141 // Internals:
142 var $sWArr = array(); // Search Words and operators
143 var $optValues = array(); // Selector box values for search configuration form
144 var $firstRow = Array(); // Will hold the first row in result - used to calculate relative hit-ratings.
145
146 var $cache_path = array(); // Caching of page path
147 var $cache_rl = array(); // Caching of root line data
148 var $fe_groups_required = array(); // Required fe_groups memberships for display of a result.
149 var $domain_records = array(); // Domain records (?)
150 var $wSelClauses = array(); // Select clauses for individual words
151 var $resultSections = array(); // Page tree sections for search result.
152 var $external_parsers = array(); // External parser objects
153 var $iconFileNameCache = array(); // Storage of icons....
154 var $lexerObj; // Lexer object
155
156
157 /**
158 * Main function, called from TypoScript as a USER_INT object.
159 *
160 * @param string Content input, ignore (just put blank string)
161 * @param array TypoScript configuration of the plugin!
162 * @return string HTML code for the search form / result display.
163 */
164 function main($content, $conf) {
165
166 // Initialize:
167 $this->conf = $conf;
168 $this->pi_loadLL();
169 $this->pi_setPiVarDefaults();
170
171 // Initialize the indexer-class - just to use a few function (for making hashes)
172 $this->indexerObj = t3lib_div::makeInstance('tx_indexedsearch_indexer');
173
174 // Initialize:
175 $this->initialize();
176
177 // Do search:
178 // If there were any search words entered...
179 if (is_array($this->sWArr)) {
180 $content = $this->doSearch($this->sWArr);
181 }
182
183 // Finally compile all the content, form, messages and results:
184 $content=
185 $this->makeSearchForm($this->optValues).
186 $this->printRules().
187 $content;
188
189 return $this->pi_wrapInBaseClass($content);
190 }
191
192 /**
193 * Initialize internal variables, especially selector box values for the search form and search words
194 *
195 * @return void
196 */
197 function initialize() {
198 global $TYPO3_CONF_VARS;
199
200 // Initialize external document parsers for icon display and other soft operations
201 if (is_array($TYPO3_CONF_VARS['EXTCONF']['indexed_search']['external_parsers'])) {
202 foreach($TYPO3_CONF_VARS['EXTCONF']['indexed_search']['external_parsers'] as $extension => $_objRef) {
203 $this->external_parsers[$extension] = &t3lib_div::getUserObj($_objRef);
204
205 // Init parser and if it returns false, unset its entry again:
206 if (!$this->external_parsers[$extension]->softInit($extension)) {
207 unset($this->external_parsers[$extension]);
208 }
209 }
210 }
211
212 // Init lexer (used to post-processing of search words)
213 $lexerObjRef = $TYPO3_CONF_VARS['EXTCONF']['indexed_search']['lexer'] ?
214 $TYPO3_CONF_VARS['EXTCONF']['indexed_search']['lexer'] :
215 'EXT:indexed_search/class.lexer.php:&tx_indexedsearch_lexer';
216 $this->lexerObj = &t3lib_div::getUserObj($lexerObjRef);
217
218 // If "_sections" is set, this value overrides any existing value.
219 if ($this->piVars['_sections']) $this->piVars['sections'] = $this->piVars['_sections'];
220
221 // Add previous search words to current
222 if ($this->piVars['sword_prev_include'] && $this->piVars['sword_prev']) {
223 $this->piVars['sword'] = trim($this->piVars['sword_prev']).' '.$this->piVars['sword'];
224 }
225
226 $this->piVars['results'] = t3lib_div::intInRange($this->piVars['results'],1,100000,$this->defaultResultNumber);
227
228 // Selector-box values defined here:
229 $this->optValues = Array(
230 'type' => Array(
231 '0' => $this->pi_getLL('opt_type_0'),
232 '1' => $this->pi_getLL('opt_type_1'),
233 '2' => $this->pi_getLL('opt_type_2'),
234 '3' => $this->pi_getLL('opt_type_3'),
235 '10' => $this->pi_getLL('opt_type_10'),
236 '20' => $this->pi_getLL('opt_type_20'),
237 ),
238 'defOp' => Array(
239 '0' => $this->pi_getLL('opt_defOp_0'),
240 '1' => $this->pi_getLL('opt_defOp_1'),
241 ),
242 'sections' => Array(
243 '0' => $this->pi_getLL('opt_sections_0'),
244 '-1' => $this->pi_getLL('opt_sections_-1'),
245 '-2' => $this->pi_getLL('opt_sections_-2'),
246 '-3' => $this->pi_getLL('opt_sections_-3'),
247 // 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.
248 ),
249 'media' => Array(
250 '-1' => $this->pi_getLL('opt_media_-1'),
251 '0' => $this->pi_getLL('opt_media_0'),
252 '-2' => $this->pi_getLL('opt_media_-2'),
253 ),
254 'order' => Array(
255 'rank_flag' => $this->pi_getLL('opt_order_rank_flag'),
256 'rank_freq' => $this->pi_getLL('opt_order_rank_freq'),
257 'rank_first' => $this->pi_getLL('opt_order_rank_first'),
258 'rank_count' => $this->pi_getLL('opt_order_rank_count'),
259 'mtime' => $this->pi_getLL('opt_order_mtime'),
260 'title' => $this->pi_getLL('opt_order_title'),
261 'crdate' => $this->pi_getLL('opt_order_crdate'),
262 ),
263 'group' => Array (
264 'sections' => $this->pi_getLL('opt_group_sections'),
265 'flat' => $this->pi_getLL('opt_group_flat'),
266 ),
267 'lang' => Array (
268 -1 => $this->pi_getLL('opt_lang_-1'),
269 0 => $this->pi_getLL('opt_lang_0'),
270 ),
271 'desc' => Array (
272 '0' => $this->pi_getLL('opt_desc_0'),
273 '1' => $this->pi_getLL('opt_desc_1'),
274 ),
275 'results' => Array (
276 '10' => '10',
277 '20' => '20',
278 '50' => '50',
279 '100' => '100',
280 )
281 );
282
283 // Add media to search in:
284 foreach($this->external_parsers as $extension => $obj) {
285 if ($name = $obj->searchTypeMediaTitle($extension)) {
286 $this->optValues['media'][$extension] = $this->pi_getLL('opt_sections_'.$extension,$name);
287 }
288 }
289
290 // Add operators for various languages
291 // Converts the operators to UTF-8 and lowercase
292 $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');
293 $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');
294 $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');
295
296 // This is the id of the site root. This value may be a commalist of integer (prepared for this)
297 $this->wholeSiteIdList = intval($GLOBALS['TSFE']->config['rootLine'][0]['uid']);
298
299 // Creating levels for section menu:
300 // This selects the first and secondary menus for the "sections" selector - so we can search in sections and sub sections.
301 if ($this->conf['show.']['L1sections']) {
302 $firstLevelMenu = $this->getMenu($this->wholeSiteIdList);
303 while(list($kk,$mR) = each($firstLevelMenu)) {
304 if ($mR['doktype']!=5) {
305 $this->optValues['sections']['rl1_'.$mR['uid']] = trim($this->pi_getLL('opt_RL1').' '.$mR['title']);
306 if ($this->conf['show.']['L2sections']) {
307 $secondLevelMenu = $this->getMenu($mR['uid']);
308 while(list($kk2,$mR2) = each($secondLevelMenu)) {
309 if ($mR['doktype']!=5) {
310 $this->optValues['sections']['rl2_'.$mR2['uid']] = trim($this->pi_getLL('opt_RL2').' '.$mR2['title']);
311 } else unset($secondLevelMenu[$kk2]);
312 }
313 $this->optValues['sections']['rl2_'.implode(',',array_keys($secondLevelMenu))] = $this->pi_getLL('opt_RL2ALL');
314 }
315 } else unset($firstLevelMenu[$kk]);
316 }
317 $this->optValues['sections']['rl1_'.implode(',',array_keys($firstLevelMenu))] = $this->pi_getLL('opt_RL1ALL');
318 }
319
320 // 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.
321 // 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...
322 if ($this->conf['search.']['rootPidList']) {
323 $this->wholeSiteIdList = implode(',',t3lib_div::intExplode(',',$this->conf['search.']['rootPidList']));
324 }
325
326 // Add search languages:
327 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'sys_language', '1=1'.$this->cObj->enableFields('sys_language'));
328 while($lR = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
329 $this->optValues['lang'][$lR['uid']] = $lR['title'];
330 }
331
332 // Calling hook for modification of initialized content
333 if ($hookObj = &$this->hookRequest('initialize_postProc')) {
334 $hookObj->initialize_postProc();
335 }
336
337 // Default values set:
338 // Setting first values in optValues as default values IF there is not corresponding piVar value set already.
339 foreach($this->optValues as $kk => $vv) {
340 if (!isset($this->piVars[$kk])) {
341 reset($vv);
342 $this->piVars[$kk] = key($vv);
343 }
344 }
345
346 // Blind selectors:
347 if (is_array($this->conf['blind.'])) {
348 foreach($this->conf['blind.'] as $kk => $vv) {
349 if (is_array($vv)) {
350 foreach($vv as $kkk => $vvv) {
351 if (!is_array($vvv) && $vvv && is_array($this->optValues[substr($kk,0,-1)])) {
352 unset($this->optValues[substr($kk,0,-1)][$kkk]);
353 }
354 }
355 } elseif ($vv) { // If value is not set, unset the option array.
356 unset($this->optValues[$kk]);
357 }
358 }
359 }
360
361 // This gets the search-words into the $sWArr:
362 $this->sWArr = $this->getSearchWords($this->piVars['defOp']);
363 }
364
365 /**
366 * 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" holds the SQL operator (eg. AND, OR)
367 *
368 * Only words with 2 or more characters are accepted
369 * Max 200 chars total
370 * Space is used to split words, "" can be used search for a whole string (not indexed search then)
371 * AND, OR and NOT are prefix words, overruling the default operator
372 * +/|/- equals AND, OR and NOT as operators.
373 * All search words are converted to lowercase.
374 *
375 * $defOp is the default operator. 1=OR, 0=AND
376 *
377 * @param boolean If true, the default operator will be OR, not AND
378 * @return array Returns array with search words if any found
379 */
380 function getSearchWords($defOp) {
381 // 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!)
382 $inSW = substr($this->piVars['sword'],0,200);
383
384 // Convert to UTF-8 + conv. entities (was also converted during indexing!)
385 $inSW = $GLOBALS['TSFE']->csConvObj->utf8_encode($inSW, $GLOBALS['TSFE']->metaCharset);
386 $inSW = $GLOBALS['TSFE']->csConvObj->entities_to_utf8($inSW,TRUE);
387
388 if ($hookObj = &$this->hookRequest('getSearchWords')) {
389 return $hookObj->getSearchWords_splitSWords($inSW, $defOp);
390 } else {
391
392 if ($this->piVars['type']==20) {
393 return array(array('sword'=>trim($inSW), 'oper'=>'AND'));
394 } else {
395 $search = t3lib_div::makeInstance('tslib_search');
396 $search->default_operator = $defOp==1 ? 'OR' : 'AND';
397 $search->operator_translate_table = $this->operator_translate_table;
398 $search->register_and_explode_search_string($inSW);
399
400 if (is_array($search->sword_array)) {
401 return $this->procSearchWordsByLexer($search->sword_array);
402 }
403 }
404 }
405 }
406
407 /**
408 * Post-process the search word array so it will match the words that was indexed (including case-folding if any)
409 * If any words are splitted into multiple words (eg. CJK will be!) the operator of the main word will remain.
410 *
411 * @param array Search word array
412 * @return array Search word array, processed through lexer
413 */
414 function procSearchWordsByLexer($SWArr) {
415
416 // Init output variable:
417 $newSWArr = array();
418
419 // Traverse the search word array:
420 foreach($SWArr as $wordDef) {
421 if (!strstr($wordDef['sword'],' ')) { // No space in word (otherwise it might be a sentense in quotes like "there is").
422 // SPlit the search word by lexer:
423 $res = $this->lexerObj->split2Words($wordDef['sword']);
424
425 // Traverse lexer result and add all words again:
426 foreach($res as $word) {
427 $newSWArr[] = array('sword'=>$word, 'oper'=>$wordDef['oper']);
428 }
429 } else {
430 $newSWArr[] = $wordDef;
431 }
432 }
433
434 // Return result:
435 return $newSWArr;
436 }
437
438
439
440
441
442
443
444
445
446 /*****************************
447 *
448 * Main functions
449 *
450 *****************************/
451
452 /**
453 * Performs the search, the display and writing stats
454 *
455 * @param array Search words in array, see ->getSearchWords() for details
456 * @return string HTML for result display.
457 */
458 function doSearch($sWArr) {
459
460 // Get result rows:
461 $pt1 = t3lib_div::milliseconds();
462 if ($hookObj = &$this->hookRequest('getResultRows')) {
463 $resData = $hookObj->getResultRows($sWArr);
464 } else {
465 $resData = $this->getResultRows($sWArr);
466 }
467
468 // Display search results:
469 $pt2 = t3lib_div::milliseconds();
470 if ($hookObj = &$this->hookRequest('getDisplayResults')) {
471 $content = $hookObj->getDisplayResults($sWArr, $resData);
472 } else {
473 $content = $this->getDisplayResults($sWArr, $resData);
474 }
475
476 $pt3 = t3lib_div::milliseconds();
477
478 // Write search statistics
479 $this->writeSearchStat($sWArr,$resData['count'],array($pt1,$pt2,$pt3));
480
481 // Return content:
482 return $content;
483 }
484
485 /**
486 * Get search result rows / data from database. Returned as data in array.
487 *
488 * @param array Search word array
489 * @return array False if no result, otherwise an array with keys for first row, result rows and total number of results found.
490 */
491 function getResultRows($sWArr) {
492
493 // Getting SQL result pointer:
494 $GLOBALS['TT']->push('Searching result');
495 $res = $this->getResultRows_SQLpointer($sWArr);
496 $GLOBALS['TT']->pull();
497
498 // Organize and process result:
499 if ($res) {
500
501 // Get some variables:
502 $count = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
503 $pointer = t3lib_div::intInRange($this->piVars['pointer'],0,floor($count/$this->piVars['results']));
504
505 // Initialize result accumulation variables:
506 $c = 0;
507 $lines = Array();
508 $grouping_phashes = array(); // Used to filter out duplicates.
509 $grouping_chashes = array(); // Used to filter out duplicates BASED ON cHash.
510 $firstRow = Array(); // Will hold the first row in result - used to calculate relative hit-ratings.
511 $resultRows = Array(); // Will hold the results rows for display.
512
513 // Now, traverse result and put the rows to be displayed into an array
514 // Each row should be a the fields from 'ISEC.*, IP.*' combined + artificial fields "show_resume" (boolean) and "result_number" (counter)
515 while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
516
517 // Set first row:
518 if (!$c) {
519 $firstRow = $row;
520 }
521
522 $row['show_resume'] = $this->checkResume($row); // Tells whether we can link directly to a document or not (depends on possible right problems)
523 $phashGr = !in_array($row['phash_grouping'], $grouping_phashes);
524 $chashGr = !in_array($row['contentHash'].'.'.$row['data_page_id'], $grouping_chashes);
525 if ($phashGr && $chashGr) {
526 if ($row['show_resume']) { // Only if the resume may be shown are we going to filter out duplicates...
527 if (!$this->multiplePagesType($row['item_type'])) { // Only on documents which are not multiple pages documents
528 $grouping_phashes[] = $row['phash_grouping'];
529 }
530 $grouping_chashes[] = $row['contentHash'].'.'.$row['data_page_id'];
531 }
532 $c++;
533
534 // All rows for display is put into resultRows[]
535 if ($c > $pointer * $this->piVars['results']) {
536 $row['result_number'] = $c;
537 $resultRows[] = $row;
538 if ($c+1 > ($pointer+1)*$this->piVars['results']) break;
539 }
540 } else {
541 $count--; // For each time a phash_grouping document is found (which is thus not displayed) the search-result count is reduced, so that it matches the number of rows displayed.
542 }
543 }
544
545 return array(
546 'resultRows' => $resultRows,
547 'firstRow' => $firstRow,
548 'count' => $count
549 );
550 } else { // No results found:
551 return FALSE;
552 }
553 }
554
555 /**
556 * Gets a SQL result pointer to traverse for the search records.
557 *
558 * @param array Search words
559 * @return pointer
560 */
561 function getResultRows_SQLpointer($sWArr) {
562 // This SEARCHES for the searchwords in $sWArr AND returns a COMPLETE list of phash-integers of the matches.
563 $list = $this->getPhashList($sWArr);
564
565 // Perform SQL Search / collection of result rows array:
566 if ($list) {
567 // Do the search:
568 return $this->execFinalQuery($list);
569 } else {
570 return FALSE;
571 }
572 }
573
574 /**
575 * Compiles the HTML display of the incoming array of result rows.
576 *
577 * @param array Search words array (for display of text describing what was searched for)
578 * @param array Array with result rows, count, first row.
579 * @return string HTML content to display result.
580 */
581 function getDisplayResults($sWArr, $resData) {
582 // Perform display of result rows array:
583 if ($resData) {
584 $GLOBALS['TT']->push('Display Final result');
585
586 // Set first selected row (for calculation of ranking later)
587 $this->firstRow = $resData['firstRow'];
588
589 // Result display here:
590 $rowcontent = '';
591 $rowcontent.= $this->compileResult($resData['resultRows']);
592
593 // Browsing box:
594 if ($resData['count']) {
595 $this->internal['res_count'] = $resData['count'];
596 $this->internal['results_at_a_time'] = $this->piVars['results'];
597 $this->internal['maxPages'] = t3lib_div::intInRange($this->conf['search.']['page_links'],1,100,10);
598 $addString = ($resData['count']&&$this->piVars['group']=='sections' ? ' '.sprintf($this->pi_getLL(count($this->resultSections)>1?'inNsections':'inNsection'),count($this->resultSections)):'');
599 $browseBox1 = $this->pi_list_browseresults(1,$addString,$this->printResultSectionLinks());
600 $browseBox2 = $this->pi_list_browseresults(0);
601 }
602
603 // Browsing nav, bottom.
604 if ($resData['count']) {
605 $content = $browseBox1.$rowcontent.$browseBox2;
606 } else {
607 $content = '<p'.$this->pi_classParam('noresults').'>'.$this->pi_getLL('noResults','',1).'</p>';
608 }
609
610 $GLOBALS['TT']->pull();
611 } else {
612 $content.='<p'.$this->pi_classParam('noresults').'>'.$this->pi_getLL('noResults','',1).'</p>';
613 }
614
615 // Print a message telling which words we searched for, and in which sections etc.
616 $what = $this->tellUsWhatIsSeachedFor($sWArr).
617 (substr($this->piVars['sections'],0,2)=='rl'?' '.$this->pi_getLL('inSection','',1).' "'.substr($this->getPathFromPageId(substr($this->piVars['sections'],4)),1).'"':'');
618 $what = '<div'.$this->pi_classParam('whatis').'><p>'.$what.'</p></div>';
619 $content = $what.$content;
620
621 // Return content:
622 return $content;
623 }
624
625 /**
626 * Takes the array with resultrows as input and returns the result-HTML-code
627 * Takes the "group" var into account: Makes a "section" or "flat" display.
628 *
629 * @param array Result rows
630 * @return string HTML
631 */
632 function compileResult($resultRows) {
633 $content = '';
634
635 // Transfer result rows to new variable, performing some mapping of sub-results etc.
636 $newResultRows = array();
637 foreach($resultRows as $row) {
638 $id = md5($row['phash_grouping']);
639 if (is_array($newResultRows[$id])) {
640 if (!$newResultRows[$id]['show_resume'] && $row['show_resume']) { // swapping:
641
642 // Remove old
643 $subrows = $newResultRows[$id]['_sub'];
644 unset($newResultRows[$id]['_sub']);
645 $subrows[] = $newResultRows[$id];
646
647 // Insert new:
648 $newResultRows[$id] = $row;
649 $newResultRows[$id]['_sub'] = $subrows;
650 } else $newResultRows[$id]['_sub'][] = $row;
651 } else {
652 $newResultRows[$id] = $row;
653 }
654 }
655 $resultRows = $newResultRows;
656
657
658 switch($this->piVars['group']) {
659 case 'sections':
660
661 $rl2flag = substr($this->piVars['sections'],0,2)=='rl';
662 $sections = array();
663 foreach($resultRows as $row) {
664 $id = $row['rl0'].'-'.$row['rl1'].($rl2flag?'-'.$row['rl2']:'');
665 $sections[$id][] = $row;
666 }
667
668 $this->resultSections = array();
669
670 foreach($sections as $id => $resultRows) {
671 $rlParts = explode('-',$id);
672 $theId = $rlParts[2] ? $rlParts[2] : ($rlParts[1]?$rlParts[1]:$rlParts[0]);
673 $theRLid = $rlParts[2] ? 'rl2_'.$rlParts[2]:($rlParts[1]?'rl1_'.$rlParts[1]:'0');
674
675 $sectionName = substr($this->getPathFromPageId($theId),1);
676 if (!trim($sectionName)) {
677 $sectionTitleLinked = $this->pi_getLL('unnamedSection','',1).':';
678 } else {
679 $onclick = 'document.'.$this->prefixId.'[\''.$this->prefixId.'[_sections]\'].value=\''.$theRLid.'\';document.'.$this->prefixId.'.submit();return false;';
680 $sectionTitleLinked = '<a href="#" onclick="'.htmlspecialchars($onclick).'">'.htmlspecialchars($sectionName).':</a>';
681 }
682 $this->resultSections[$id] = array($sectionName,count($resultRows));
683
684 // Add content header:
685 $content.= $this->makeSectionHeader($id,$sectionTitleLinked,count($resultRows));
686
687 // Render result rows:
688 foreach($resultRows as $row) {
689 $content.= $this->printResultRow($row);
690 }
691 }
692 break;
693 default: // flat:
694 foreach($resultRows as $row) {
695 $content.= $this->printResultRow($row);
696 }
697 break;
698 }
699 return '<div'.$this->pi_classParam('res').'>'.$content.'</div>';
700 }
701
702
703
704
705
706
707
708
709
710
711
712 /***********************************
713 *
714 * Searching functions (SQL)
715 *
716 ***********************************/
717
718 /**
719 * Returns a COMPLETE list of phash-integers matching the search-result composed of the search-words in the sWArr array.
720 * The list of phash integers are unsorted and should be used for subsequent selection of index_phash records for display of the result.
721 *
722 * @param array Search word array
723 * @return string List of integers
724 */
725 function getPhashList($sWArr) {
726
727 // Initialize variables:
728 $c=0;
729 $totalHashList = array(); // This array accumulates the phash-values
730 $this->wSelClauses = array();
731
732 // Traverse searchwords; for each, select all phash integers and merge/diff/intersect them with previous word (based on operator)
733 foreach($sWArr as $k => $v) {
734 $GLOBALS['TT']->push('SearchWord '.$sWord);
735
736 // Making the query for a single search word based on the search-type
737 $sWord = $v['sword']; // $GLOBALS['TSFE']->csConvObj->conv_case('utf-8',$v['sword'],'toLower'); // lower-case all of them...
738
739 $theType = (string)$this->piVars['type'];
740 if (strstr($sWord,' ')) $theType = 20; // If there are spaces in the search-word, make a full text search instead.
741 $res = '';
742 $wSel='';
743
744 // Perform search for word:
745 switch($theType) {
746 case '1':
747 $wSel = "IW.baseword LIKE '%".$GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_words')."%'";
748 $res = $this->execPHashListQuery($wSel,' AND is_stopword=0');
749 break;
750 case '2':
751 $wSel = "IW.baseword LIKE '".$GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_words')."%'";
752 $res = $this->execPHashListQuery($wSel,' AND is_stopword=0');
753 break;
754 case '3':
755 $wSel = "IW.baseword LIKE '%".$GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_words')."'";
756 $res = $this->execPHashListQuery($wSel,' AND is_stopword=0');
757 break;
758 case '10':
759 $wSel = 'IW.metaphone = '.$this->indexerObj->metaphone($sWord);
760 $res = $this->execPHashListQuery($wSel,' AND is_stopword=0');
761 break;
762 case '20':
763 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
764 'ISEC.phash',
765 'index_section ISEC, index_fulltext IFT',
766 'IFT.fulltextdata LIKE \'%'.$GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_fulltext').'%\' AND
767 ISEC.phash = IFT.phash
768 '.$this->sectionTableWhere(),
769 'ISEC.phash'
770 );
771 $wSel = '1=1';
772
773 if ($this->piVars['type']==20) $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 instaed. It is not required, but otherwise some hits may be left out.
774 break;
775 default:
776 $wSel = 'IW.wid = '.$hash = $this->indexerObj->md5inthash($sWord);
777 $res = $this->execPHashListQuery($wSel,' AND is_stopword=0');
778 break;
779 }
780
781 // Accumulate the word-select clauses
782 $this->wSelClauses[] = $wSel;
783
784 // If there was a query to do, then select all phash-integers which resulted from this.
785 if ($res) {
786
787 // Get phash list by searching for it:
788 $phashList = array();
789 while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
790 $phashList[] = $row['phash'];
791 }
792 $GLOBALS['TYPO3_DB']->sql_free_result($res);
793
794 // Here the phash list are merged with the existing result based on whether we are dealing with OR, NOT or AND operations.
795 if ($c) {
796 switch($v['oper']) {
797 case 'OR':
798 $totalHashList = array_unique(array_merge($phashList,$totalHashList));
799 break;
800 case 'AND NOT':
801 $totalHashList = array_diff($totalHashList,$phashList);
802 break;
803 default: // AND...
804 $totalHashList = array_intersect($totalHashList,$phashList);
805 break;
806 }
807 } else {
808 $totalHashList = $phashList; // First search
809 }
810 }
811
812 $GLOBALS['TT']->pull();
813 $c++;
814 }
815
816 return implode(',',$totalHashList);
817 }
818
819 /**
820 * Returns a query which selects the search-word from the word/rel tables.
821 *
822 * @param string WHERE clause selecting the word from phash
823 * @param string Additional AND clause in the end of the query.
824 * @return pointer SQL result pointer
825 */
826 function execPHashListQuery($wordSel,$plusQ='') {
827 return $GLOBALS['TYPO3_DB']->exec_SELECTquery(
828 'IR.phash',
829 'index_words IW,
830 index_rel IR,
831 index_section ISEC',
832 $wordSel.'
833 AND IW.wid=IR.wid
834 AND ISEC.phash = IR.phash
835 '.$this->sectionTableWhere().'
836 '.$plusQ,
837 'IR.phash'
838 );
839 }
840
841 /**
842 * Returns AND statement for selection of section in database. (rootlevel 0-2 + page_id)
843 *
844 * @return string AND clause for selection of section in database.
845 */
846 function sectionTableWhere() {
847 $out = $this->wholeSiteIdList<0 ? '' : 'AND ISEC.rl0 IN ('.$this->wholeSiteIdList.')';
848
849 $match = '';
850 if (substr($this->piVars['sections'],0,4)=='rl1_') {
851 $list = implode(',',t3lib_div::intExplode(',',substr($this->piVars['sections'],4)));
852 $out.= 'AND ISEC.rl1 IN ('.$list.')';
853 $match = TRUE;
854 } elseif (substr($this->piVars['sections'],0,4)=='rl2_') {
855 $list = implode(',',t3lib_div::intExplode(',',substr($this->piVars['sections'],4)));
856 $out.= 'AND ISEC.rl2 IN ('.$list.')';
857 $match = TRUE;
858 } elseif (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'])) {
859 // Traversing user configured fields to see if any of those are used to limit search to a section:
860 foreach($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'] as $fieldName => $rootLineLevel) {
861 if (substr($this->piVars['sections'],0,strlen($fieldName)+1)==$fieldName.'_') {
862 $list = implode(',',t3lib_div::intExplode(',',substr($this->piVars['sections'],strlen($fieldName)+1)));
863 $out.= 'AND ISEC.'.$fieldName.' IN ('.$list.')';
864 $match = TRUE;
865 break;
866 }
867 }
868 }
869
870 // If no match above, test the static types:
871 if (!$match) {
872 switch((string)$this->piVars['sections']) {
873 case '-1': // '-1' => 'Only this page',
874 $out.= ' AND ISEC.page_id='.$GLOBALS['TSFE']->id;
875 break;
876 case '-2': // '-2' => 'Top + level 1',
877 $out.= ' AND ISEC.rl2=0';
878 break;
879 case '-3': // '-3' => 'Level 2 and out',
880 $out.= ' AND ISEC.rl2>0';
881 break;
882 }
883 }
884
885 return $out;
886 }
887
888 /**
889 * Returns AND statement for selection of media type
890 *
891 * @return string AND statement for selection of media type
892 */
893 function mediaTypeWhere() {
894
895 switch((string)$this->piVars['media']) {
896 case '0': // '0' => 'Kun TYPO3 sider',
897 $out = 'AND IP.item_type='.$GLOBALS['TYPO3_DB']->fullQuoteStr('0', 'index_phash');;
898 break;
899 case '-2': // All external documents
900 $out = 'AND IP.item_type!='.$GLOBALS['TYPO3_DB']->fullQuoteStr('0', 'index_phash');;
901 break;
902 case '-1': // All content
903 $out='';
904 break;
905 default:
906 $out = 'AND IP.item_type='.$GLOBALS['TYPO3_DB']->fullQuoteStr($this->piVars['media'], 'index_phash');
907 break;
908 }
909
910 return $out;
911 }
912
913 /**
914 * Returns AND statement for selection of langauge
915 *
916 * @return string AND statement for selection of langauge
917 */
918 function languageWhere() {
919 if ($this->piVars['lang']>=0) { // -1 is the same as ALL language.
920 return 'AND IP.sys_language_uid='.intval($this->piVars['lang']);
921 }
922 }
923
924 /**
925 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
926 *
927 * @param string List of phash integers which match the search.
928 * @return pointer Query result pointer
929 */
930 function execFinalQuery($list) {
931
932 // Setting up methods of filtering results based on page types, access, etc.
933 $page_join = '';
934 $page_where = '';
935
936 // Calling hook for alternative creation of page ID list
937 if ($hookObj = &$this->hookRequest('execFinalQuery_idList')) {
938 $page_where = $hookObj->execFinalQuery_idList($list);
939 } elseif ($this->join_pages) { // Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
940 $page_join = ',
941 pages';
942 $page_where = 'pages.uid = ISEC.page_id
943 '.$this->cObj->enableFields('pages').'
944 AND pages.no_search=0
945 AND pages.doktype<200
946 ';
947 } 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!
948 $siteIdNumbers = t3lib_div::intExplode(',',$this->wholeSiteIdList);
949 $id_list=array();
950 while(list(,$rootId)=each($siteIdNumbers)) {
951 $id_list[] = $this->cObj->getTreeList($rootId,9999,0,0,'','').$rootId;
952 }
953 $page_where = 'ISEC.page_id IN ('.implode(',',$id_list).')';
954 } else { // Disable everything... (select all)
955 $page_where = ' 1=1 ';
956 }
957
958
959 // 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.
960 if (substr($this->piVars['order'],0,5)=='rank_') {
961 /*
962 OK there were some fancy calculations promoted by Graeme Merrall:
963
964 "However, regarding relevance you probably want to look at something like
965 Salton's formula which is a good easy way to measure relevance.
966 Oracle Intermedia uses this and it's pretty simple:
967 Score can be between 0 and 100, but the top-scoring document in the query
968 will not necessarily have a score of 100 -- scoring is relative, not
969 absolute. This means that scores are not comparable across indexes, or even
970 across different queries on the same index. Score for each document is
971 computed using the standard Salton formula:
972
973 3f(1+log(N/n))
974
975 Where f is the frequency of the search term in the document, N is the total
976 number of rows in the table, and n is the number of rows which contain the
977 search term. This is converted into an integer in the range 0 - 100.
978
979 There's a good doc on it at
980 http://ls6-www.informatik.uni-dortmund.de/bib/fulltext/ir/Pfeifer:97/
981 although it may be a little complex for what you require so just pick the
982 relevant parts out.
983 "
984
985 However I chose not to go with this for several reasons.
986 I do not claim that my ways of calculating importance here is the best.
987 ANY (better) suggestion for ranking calculation is accepted! (as long as they are shipped with tested code in exchange for this.)
988 */
989
990 switch($this->piVars['order']) {
991 case 'rank_flag': // This gives priority to word-position (max-value) so that words in title, keywords, description counts more than in content.
992 // The ordering is refined with the frequency sum as well.
993 $grsel = 'MAX(IR.flags) AS order_val1, SUM(IR.freq) AS order_val2';
994 $orderBy = 'order_val1'.$this->isDescending().',order_val2'.$this->isDescending();
995 break;
996 case 'rank_first': // Results in average position of search words on page. Must be inversely sorted (low numbers are closer to top)
997 $grsel = 'AVG(IR.first) AS order_val';
998 $orderBy = 'order_val'.$this->isDescending(1);
999 break;
1000 case 'rank_count': // Number of words found
1001 $grsel = 'SUM(IR.count) AS order_val';
1002 $orderBy = 'order_val'.$this->isDescending();
1003 break;
1004 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?
1005 $grsel = 'SUM(IR.freq) AS order_val';
1006 $orderBy = 'order_val'.$this->isDescending();
1007 break;
1008 }
1009
1010 // So, words are imploded into an OR statement (no "sentence search" should be done here - may deselect results)
1011 $wordSel='('.implode(' OR ',$this->wSelClauses).') AND ';
1012
1013 return $GLOBALS['TYPO3_DB']->exec_SELECTquery(
1014 'ISEC.*, IP.*, '
1015 .$grsel,
1016 'index_words IW,
1017 index_rel IR,
1018 index_section ISEC,
1019 index_phash IP'.
1020 $page_join,
1021 $wordSel.'
1022 IP.phash IN ('.$list.') '.
1023 $this->mediaTypeWhere().' '.
1024 $this->languageWhere().'
1025 AND IW.wid=IR.wid
1026 AND ISEC.phash = IR.phash
1027 AND IP.phash = IR.phash
1028 AND '.$page_where,
1029 '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',
1030 $orderBy
1031 );
1032 } else { // Otherwise, if sorting are done with the pages table or other fields, there is no need for joining with the rel/word tables:
1033
1034 $orderBy = '';
1035 switch((string)$this->piVars['order']) {
1036 case 'title':
1037 $orderBy = 'IP.item_title'.$this->isDescending();
1038 break;
1039 case 'crdate':
1040 $orderBy = 'IP.item_crdate'.$this->isDescending();
1041 break;
1042 case 'mtime':
1043 $orderBy = 'IP.item_mtime'.$this->isDescending();
1044 break;
1045 }
1046
1047 return $GLOBALS['TYPO3_DB']->exec_SELECTquery(
1048 'ISEC.*, IP.*',
1049 'index_phash IP,index_section ISEC'.$page_join,
1050 'IP.phash IN ('.$list.') '.
1051 $this->mediaTypeWhere().' '.
1052 $this->languageWhere().'
1053 AND IP.phash = ISEC.phash
1054 AND '.$page_where,
1055 'IP.phash',
1056 $orderBy
1057 );
1058 }
1059 }
1060
1061 /**
1062 * Checking if the resume can be shown for the search result (depending on whether the rights are OK)
1063 * ? Should it also check for gr_list "0,-1"?
1064 *
1065 * @param array Result row array.
1066 * @return boolean Returns true if resume can safely be shown
1067 */
1068 function checkResume($row) {
1069
1070 // If the record is indexed by an indexing configuration, just show it.
1071 // At least this is needed for external URLs and files.
1072 // For records we might need to extend this - for instance block display if record is access restricted.
1073 if ($row['freeIndexUid']) {
1074 return TRUE;
1075 }
1076
1077 // Evaluate regularly indexed pages based on item_type:
1078 if ($row['item_type']) { // External media:
1079 // For external media we will check the access of the parent page on which the media was linked from.
1080 // "phash_t3" is the phash of the parent TYPO3 page row which initiated the indexing of the documents in this section.
1081 // So, selecting for the grlist records belonging to the parent phash-row where the current users gr_list exists will help us to know.
1082 // 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.
1083 $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'));
1084 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res)) {
1085 #debug("Look up for external media '".$row['data_filename']."': phash:".$row['phash_t3'].' YES - ('.$GLOBALS['TSFE']->gr_list.")!",1);
1086 return TRUE;
1087 } else {
1088 #debug("Look up for external media '".$row['data_filename']."': phash:".$row['phash_t3'].' NO - ('.$GLOBALS['TSFE']->gr_list.")!",1);
1089 return FALSE;
1090 }
1091 } else { // Ordinary TYPO3 pages:
1092 if (strcmp($row['gr_list'],$GLOBALS['TSFE']->gr_list)) {
1093 // 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...
1094 $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'));
1095 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res)) {
1096 #debug('Checking on it ...'.$row['item_title'].'/'.$row['phash'].' - YES ('.$GLOBALS['TSFE']->gr_list.")",1);
1097 return TRUE;
1098 } else {
1099 #debug('Checking on it ...'.$row['item_title'].'/'.$row['phash']." - NOPE",1);
1100 return FALSE;
1101 }
1102 } else {
1103 #debug('Resume can be shown, because the document was in fact indexed by this combination of groups!'.$GLOBALS['TSFE']->gr_list.' - '.$row['item_title'].'/'.$row['phash'],1);
1104 return TRUE;
1105 }
1106 }
1107 }
1108
1109 /**
1110 * Returns "DESC" or "" depending on the settings of the incoming highest/lowest result order (piVars['desc']
1111 *
1112 * @param boolean If true, inverse the order which is defined by piVars['desc']
1113 * @return string " DESC" or ""
1114 */
1115 function isDescending($inverse=FALSE) {
1116 $desc = $this->piVars['desc'];
1117 if ($inverse) $desc=!$desc;
1118 return !$desc ? ' DESC':'';
1119 }
1120
1121 /**
1122 * Write statistics information to database for the search operation
1123 *
1124 * @param array Search Word array
1125 * @param integer Number of hits
1126 * @param integer Milliseconds the search took
1127 * @return void
1128 */
1129 function writeSearchStat($sWArr,$count,$pt) {
1130 $insertFields = array(
1131 'searchstring' => $this->piVars['sword'],
1132 'searchoptions' => serialize(array($this->piVars,$sWArr,$pt)),
1133 'feuser_id' => intval($this->fe_user->user['uid']), // fe_user id, integer
1134 'cookie' => $this->fe_user->id, // cookie as set or retrieve. If people has cookies disabled this will vary all the time...
1135 'IP' => t3lib_div::getIndpEnv('REMOTE_ADDR'), // Remote IP address
1136 'hits' => intval($count), // Number of hits on the search.
1137 'tstamp' => $GLOBALS['EXEC_TIME'] // Time stamp
1138 );
1139
1140 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_search', $insertFields);
1141 $newId = $GLOBALS['TYPO3_DB']->sql_insert_id();
1142
1143 if ($newId) {
1144 foreach($sWArr as $val) {
1145 $insertFields = array(
1146 'word' => $val['sword'], // $GLOBALS['TSFE']->csConvObj->conv_case('utf-8', $val['sword'], 'toLower'),
1147 'index_stat_search_id' => $newId,
1148 'tstamp' => $GLOBALS['EXEC_TIME'] // Time stamp
1149 );
1150
1151 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_word', $insertFields);
1152 }
1153 }
1154 }
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168 /***********************************
1169 *
1170 * HTML output functions
1171 *
1172 ***********************************/
1173
1174 /**
1175 * Make search form HTML
1176 *
1177 * @param array Value/Labels pairs for search form selector boxes.
1178 * @return string Search form HTML
1179 */
1180 function makeSearchForm($optValues) {
1181
1182 // Accumulate table rows here:
1183 $rows = array();
1184
1185 // Adding search field and button:
1186 $rows[]='<tr>
1187 <td nowrap="nowrap"><p>'.$this->pi_getLL('form_searchFor','',1).'&nbsp;</p></td>
1188 <td><input type="text" name="'.$this->prefixId.'[sword]" value="'.htmlspecialchars($this->conf['show.']['clearSearchBox']?'':$this->piVars['sword']).'"'.$this->pi_classParam('searchbox-sword').' />&nbsp;&nbsp;<input type="submit" name="'.$this->prefixId.'[submit_button]" value="'.$this->pi_getLL('submit_button_label','',1).'"'.$this->pi_classParam('searchbox-button').' /></td>
1189 </tr>';
1190
1191 if ($this->conf['show.']['clearSearchBox'] && $this->conf['show.']['clearSearchBox.']['enableSubSearchCheckBox']) {
1192 $rows[]='<tr>
1193 <td></td>
1194 <td><input type="hidden" name="'.$this->prefixId.'[sword_prev]" value="'.htmlspecialchars($this->piVars['sword']).'" /><input type="checkbox" name="'.$this->prefixId.'[sword_prev_include]" value="1"'.($this->piVars['sword_prev_include']?' checked="checked"':'').' /> '.$this->pi_getLL('makerating_addToCurrentSearch').'</td>
1195 </tr>';
1196 }
1197
1198 // Extended search options:
1199 if ($this->piVars['ext']) {
1200 if (is_array($optValues['type']) || is_array($optValues['defOp'])) $rows[]='<tr>
1201 <td nowrap="nowrap"><p>'.$this->pi_getLL('form_match','',1).'&nbsp;</p></td>
1202 <td>'.$this->renderSelectBox($this->prefixId.'[type]',$this->piVars['type'],$optValues['type']).
1203 $this->renderSelectBox($this->prefixId.'[defOp]',$this->piVars['defOp'],$optValues['defOp']).'</td>
1204 </tr>';
1205 if (is_array($optValues['media']) || is_array($optValues['lang'])) $rows[]='<tr>
1206 <td nowrap="nowrap"><p>'.$this->pi_getLL('form_searchIn','',1).'&nbsp;</p></td>
1207 <td>'.$this->renderSelectBox($this->prefixId.'[media]',$this->piVars['media'],$optValues['media']).
1208 $this->renderSelectBox($this->prefixId.'[lang]',$this->piVars['lang'],$optValues['lang']).'</td>
1209 </tr>';
1210 if (is_array($optValues['sections'])) $rows[]='<tr>
1211 <td nowrap="nowrap"><p>'.$this->pi_getLL('form_fromSection','',1).'&nbsp;</p></td>
1212 <td>'.$this->renderSelectBox($this->prefixId.'[sections]',$this->piVars['sections'],$optValues['sections']).'</td>
1213 </tr>';
1214 if (is_array($optValues['order']) || is_array($optValues['desc']) || is_array($optValues['results'])) $rows[]='<tr>
1215 <td nowrap="nowrap"><p>'.$this->pi_getLL('form_orderBy','',1).'&nbsp;</p></td>
1216 <td><p>'.$this->renderSelectBox($this->prefixId.'[order]',$this->piVars['order'],$optValues['order']).
1217 $this->renderSelectBox($this->prefixId.'[desc]',$this->piVars['desc'],$optValues['desc']).
1218 $this->renderSelectBox($this->prefixId.'[results]',$this->piVars['results'],$optValues['results']).'&nbsp;'.$this->pi_getLL('form_atATime','',1).'</p></td>
1219 </tr>';
1220 if (is_array($optValues['group']) || !$this->conf['blind.']['extResume']) $rows[]='<tr>
1221 <td nowrap="nowrap"><p>'.$this->pi_getLL('form_style','',1).'&nbsp;</p></td>
1222 <td><p>'.$this->renderSelectBox($this->prefixId.'[group]',$this->piVars['group'],$optValues['group']).
1223 (!$this->conf['blind.']['extResume'] ? '&nbsp; &nbsp;
1224 <input type="hidden" name="'.$this->prefixId.'[extResume]" value="0" /><input type="checkbox" value="1" name="'.$this->prefixId.'[extResume]"'.($this->piVars['extResume']?' checked="checked"':'').' />'.$this->pi_getLL('form_extResume','',1):'').'</p></td>
1225 </tr>';
1226 }
1227
1228 // Compile rows into a table, wrapped in form-tags:
1229 $out='
1230 <form action="'.htmlspecialchars($this->pi_getPageLink($GLOBALS['TSFE']->id,$GLOBALS['TSFE']->sPre)).'" method="post" name="'.$this->prefixId.'" style="margin: 0 0 0 0;">
1231 <table '.$this->conf['tableParams.']['searchBox'].'>
1232 '.implode(chr(10),$rows).'
1233 </table>
1234 <input type="hidden" name="'.$this->prefixId.'[_sections]" value="0" />
1235 <input type="hidden" name="'.$this->prefixId.'[pointer]" value="0" />
1236 <input type="hidden" name="'.$this->prefixId.'[ext]" value="'.($this->piVars['ext']?1:0).'" />
1237 </form>';
1238 $out.='<p>'.
1239 ($this->piVars['ext'] ?
1240 '<a href="'.htmlspecialchars($this->pi_getPageLink($GLOBALS['TSFE']->id,$GLOBALS['TSFE']->sPre,array($this->prefixId.'[ext]'=>0))).'">'.$this->pi_getLL('link_regularSearch','',1).'</a>' :
1241 '<a href="'.htmlspecialchars($this->pi_getPageLink($GLOBALS['TSFE']->id,$GLOBALS['TSFE']->sPre,array($this->prefixId.'[ext]'=>1))).'">'.$this->pi_getLL('link_advancedSearch','',1).'</a>'
1242 ).'</p>';
1243
1244 return '<div'.$this->pi_classParam('searchbox').'>'.$out.'</div>';
1245 }
1246
1247 /**
1248 * Print the searching rules
1249 *
1250 * @return string Rules for the search
1251 */
1252 function printRules() {
1253 $out = '';
1254 if ($this->conf['show.']['rules']) {
1255 $out = '<h2>'.$this->pi_getLL('rules_header','',1).'</h2>
1256 <p>'.nl2br(trim($this->pi_getLL('rules_text','',1))).'</p>';
1257 $out = '
1258 <div'.$this->pi_classParam('rules').'>'.$this->cObj->stdWrap($out, $this->conf['rules_stdWrap.']).'</div>';
1259 }
1260 return $out;
1261 }
1262
1263 /**
1264 * Returns the anchor-links to the sections inside the displayed result rows.
1265 *
1266 * @return string
1267 */
1268 function printResultSectionLinks() {
1269 if (count($this->resultSections)) {
1270 $lines = array();
1271
1272 foreach($this->resultSections as $id => $dat) {
1273 $lines[] = '<li><a href="'.htmlspecialchars($GLOBALS['TSFE']->anchorPrefix.'#'.md5($id)).'">'.
1274 htmlspecialchars(trim($dat[0])?trim($dat[0]):$this->pi_getLL('unnamedSection')).' ('.$dat[1].' '.$this->pi_getLL($dat[1]>1?'word_pages':'word_page','',1).')'.
1275 '</a></li>';
1276 }
1277 $out = '<ul>'.implode(chr(10),$lines).'</ul>';
1278 return '<div'.$this->pi_classParam('sectionlinks').'>'.$this->cObj->stdWrap($out, $this->conf['sectionlinks_stdWrap.']).'</div>';
1279 }
1280 }
1281
1282 /**
1283 * Returns the section header of the search result.
1284 *
1285 * @param string ID for the section (used for anchor link)
1286 * @param string Section title with linked wrapped around
1287 * @param integer Number of results in section
1288 * @return string HTML output
1289 */
1290 function makeSectionHeader($id,$sectionTitleLinked,$countResultRows) {
1291 return '<div'.$this->pi_classParam('secHead').'><a name="'.md5($id).'"></a><table '.$this->conf['tableParams.']['secHead'].'>
1292 <tr>
1293 <td width="95%"><h2>'.$sectionTitleLinked.'</h2></td>
1294 <td align="right" nowrap="nowrap"><p>'.$countResultRows.' '.$this->pi_getLL($countResultRows>1?'word_pages':'word_page','',1).'</p></td>
1295 </tr>
1296 </table></div>';
1297 }
1298
1299 /**
1300 * This prints a single result row, including a recursive call for subrows.
1301 *
1302 * @param array Search result row
1303 * @param integer 1=Display only header (for sub-rows!), 2=nothing at all
1304 * @return string HTML code
1305 */
1306 function printResultRow($row, $headerOnly=0) {
1307
1308 // Get template content:
1309 $tmplContent = $this->prepareResultRowTemplateData($row, $headerOnly);
1310
1311 if ($hookObj = &$this->hookRequest('printResultRow')) {
1312 return $hookObj->printResultRow($row, $headerOnly, $tmplContent);
1313 } else {
1314
1315 // Make the header row with title, icon and rating bar.:
1316 $out.='<tr '.$this->pi_classParam('title'.$tmplContent['CSSsuffix']).'>
1317 <td width="16">'.$tmplContent['icon'].'</td>
1318 <td width="95%" nowrap="nowrap"><p>'.
1319 #$row['phash'].' // '.
1320 $tmplContent['result_number'].': '.
1321 $tmplContent['title'].
1322 '</p></td>
1323 <td nowrap="nowrap"><p'.$this->pi_classParam('percent'.$tmplContent['CSSsuffix']).'>'.$tmplContent['rating'].'</p></td>
1324 </tr>';
1325
1326 // Print the resume-section. If headerOnly is 1, then only the short resume is printed
1327 if (!$headerOnly) {
1328 $out.='<tr>
1329 <td></td>
1330 <td colspan="2"'.$this->pi_classParam('descr'.$tmplContent['CSSsuffix']).'><p>'.$tmplContent['description'].'</p></td>
1331 </tr>';
1332 $out.='<tr>
1333 <td></td>
1334 <td '.$this->pi_classParam('info'.$tmplContent['CSSsuffix']).' nowrap="nowrap"><p>'.
1335 $tmplContent['size'].' - '.$tmplContent['created'].' - '.$tmplContent['modified'].
1336 ($tmplContent['path'] ? '<br/>'.$this->pi_getLL('res_path','',1).' '.$tmplContent['path'] : '').
1337 '</p></td>
1338 <td '.$this->pi_classParam('info'.$tmplContent['CSSsuffix']).' align="right"><p>'.$tmplContent['access'].$tmplContent['language'].'</p></td>
1339 </tr>';
1340 } elseif ($headerOnly==1) {
1341 $out.='<tr>
1342 <td></td>
1343 <td colspan="2"'.$this->pi_classParam('descr'.$tmplContent['CSSsuffix']).'><p>'.$tmplContent['description'].'</p></td>
1344 </tr>';
1345 }
1346
1347 // If there are subrows (eg. subpages in a PDF-file or if a duplicate page is selected due to user-login (phash_grouping))
1348 if (is_array($row['_sub'])) {
1349 if ($this->multiplePagesType($row['item_type'])) {
1350 $out.='<tr>
1351 <td></td>
1352 <td colspan="2"><p><br/>'.$this->pi_getLL('res_otherMatching','',1).'<br/><br/></p></td>
1353 </tr>';
1354
1355 foreach($row['_sub'] as $subRow) {
1356 $out.='<tr>
1357 <td></td>
1358 <td colspan="2"><p>'.$this->printResultRow($subRow,1).'</p></td>
1359 </tr>';
1360 }
1361 } else {
1362 $out.='<tr>
1363 <td></td>
1364 <td colspan="2"><p>'.$this->pi_getLL('res_otherPageAsWell','',1).'</p></td>
1365 </tr>';
1366 }
1367 }
1368
1369 return '<table '.$this->conf['tableParams.']['searchRes'].'>'.$out.'</table><br/>';
1370 }
1371 }
1372
1373 /**
1374 * Returns a results browser
1375 *
1376 * @param boolean Show result count
1377 * @param string String appended to "displaying results..." notice.
1378 * @param string String appended after section "displaying results..."
1379 * @return string HTML output
1380 */
1381 function pi_list_browseresults($showResultCount=1,$addString='',$addPart='') {
1382
1383 // Initializing variables:
1384 $pointer=$this->piVars['pointer'];
1385 $count=$this->internal['res_count'];
1386 $results_at_a_time = t3lib_div::intInRange($this->internal['results_at_a_time'],1,1000);
1387 $maxPages = t3lib_div::intInRange($this->internal['maxPages'],1,100);
1388 $max = t3lib_div::intInRange(ceil($count/$results_at_a_time),1,$maxPages);
1389 $pointer=intval($pointer);
1390 $links=array();
1391
1392 // Make browse-table/links:
1393 if ($pointer>0) {
1394 $links[]='<td><p>'.$this->makePointerSelector_link($this->pi_getLL('pi_list_browseresults_prev','< Previous',1),$pointer-1).'</p></td>';
1395 }
1396 for($a=0;$a<$max;$a++) {
1397 $links[]='<td'.($pointer==$a?$this->pi_classParam('browsebox-SCell'):'').'><p>'.$this->makePointerSelector_link(trim($this->pi_getLL('pi_list_browseresults_page','Page',1).' '.($a+1)),$a).'</p></td>';
1398 }
1399 if ($pointer<ceil($count/$results_at_a_time)-1) {
1400 $links[]='<td><p>'.$this->makePointerSelector_link($this->pi_getLL('pi_list_browseresults_next','Next >',1),$pointer+1).'</p></td>';
1401 }
1402
1403 $pR1 = $pointer*$results_at_a_time+1;
1404 $pR2 = $pointer*$results_at_a_time+$results_at_a_time;
1405 $sTables = '<div'.$this->pi_classParam('browsebox').'>'.
1406 ($showResultCount ? '<p>'.sprintf(
1407 str_replace('###SPAN_BEGIN###','<span'.$this->pi_classParam('browsebox-strong').'>',$this->pi_getLL('pi_list_browseresults_displays','Displaying results ###SPAN_BEGIN###%s to %s</span> out of ###SPAN_BEGIN###%s</span>')),
1408 $pR1,
1409 min(array($this->internal['res_count'],$pR2)),
1410 $this->internal['res_count']
1411 ).$addString.'</p>':''
1412 ).$addPart.
1413 '<table>
1414 <tr>'.implode('',$links).'</tr>
1415 </table></div>';
1416
1417 return $sTables;
1418 }
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431 /***********************************
1432 *
1433 * Support functions for HTML output (with a minimum of fixed markup)
1434 *
1435 ***********************************/
1436
1437 /**
1438 * Preparing template data for the result row output
1439 *
1440 * @param array Result row
1441 * @param boolean If set, display only header of result (for sub-results)
1442 * @return array Array with data to insert in result row template
1443 */
1444 function prepareResultRowTemplateData($row, $headerOnly) {
1445
1446 // Initialize:
1447 $specRowConf = $this->getSpecialConfigForRow($row);
1448 $CSSsuffix = $specRowConf['CSSsuffix']?'-'.$specRowConf['CSSsuffix']:'';
1449
1450 // If external media, link to the media-file instead.
1451 if ($row['item_type']) { // External media
1452 if ($row['show_resume']) { // Can link directly.
1453 $title = '<a href="'.htmlspecialchars($row['data_filename']).'">'.htmlspecialchars($this->makeTitle($row)).'</a>';
1454 } else { // Suspicious, so linking to page instead...
1455 $copy_row = $row;
1456 unset($copy_row['cHashParams']);
1457 $title = $this->linkPage($row['page_id'],htmlspecialchars($this->makeTitle($row)),$copy_row);
1458 }
1459 } else { // Else the page:
1460 $title = $this->linkPage($row['data_page_id'],htmlspecialchars($this->makeTitle($row)),$row);
1461 }
1462
1463 $tmplContent = array();
1464 $tmplContent['title'] = $title;
1465 $tmplContent['result_number'] = $row['result_number'];
1466 $tmplContent['icon'] = $this->makeItemTypeIcon($row['item_type'],'',$specRowConf);
1467 $tmplContent['rating'] = $this->makeRating($row);
1468 $tmplContent['description'] = $this->makeDescription($row,$this->piVars['extResume'] && !$headerOnly?0:1);
1469 $tmplContent = $this->makeInfo($row,$tmplContent);
1470 $tmplContent['access'] = $this->makeAccessIndication($row['page_id']);
1471 $tmplContent['language'] = $this->makeLanguageIndication($row);
1472 $tmplContent['CSSsuffix'] = $CSSsuffix;
1473
1474 // Post processing with hook.
1475 if ($hookObj = &$this->hookRequest('prepareResultRowTemplateData_postProc')) {
1476 $tmplContent = $hookObj->prepareResultRowTemplateData_postProc($tmplContent, $row, $headerOnly);
1477 }
1478
1479 return $tmplContent;
1480 }
1481
1482 /**
1483 * Returns a string that tells which search words are searched for.
1484 *
1485 * @param array Array of search words
1486 * @return string HTML telling what is searched for.
1487 */
1488 function tellUsWhatIsSeachedFor($sWArr) {
1489
1490 // Init:
1491 $searchingFor = '';
1492 $c=0;
1493
1494 // Traverse search words:
1495 foreach($sWArr as $k => $v) {
1496 if ($c) {
1497 switch($v['oper']) {
1498 case 'OR':
1499 $searchingFor.= ' '.$this->pi_getLL('searchFor_or','',1).' '.$this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1500 break;
1501 case 'AND NOT':
1502 $searchingFor.= ' '.$this->pi_getLL('searchFor_butNot','',1).' '.$this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1503 break;
1504 default: // AND...
1505 $searchingFor.= ' '.$this->pi_getLL('searchFor_and','',1).' '.$this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1506 break;
1507 }
1508 } else {
1509 $searchingFor = $this->pi_getLL('searchFor','',1).' '.$this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1510 }
1511 $c++;
1512 }
1513 return $searchingFor;
1514 }
1515
1516 /**
1517 * Wraps the search words in the search-word list display (from ->tellUsWhatIsSeachedFor())
1518 *
1519 * @param string search word to wrap (in local charset!)
1520 * @return string Search word wrapped in <span> tag.
1521 */
1522 function wrapSW($str) {
1523 return '"<span'.$this->pi_classParam('sw').'>'.htmlspecialchars($str).'</span>"';
1524 }
1525
1526 /**
1527 * Makes a selector box
1528 *
1529 * @param string Name of selector box
1530 * @param string Current value
1531 * @param array Array of options in the selector box (value => label pairs)
1532 * @return string HTML of selector box
1533 */
1534 function renderSelectBox($name,$value,$optValues) {
1535 if (is_array($optValues)) {
1536 $opt = array();
1537 $isSelFlag = 0;
1538
1539 foreach($optValues as $k => $v) {
1540 $sel = (!strcmp($k,$value) ? ' selected="selected"' : '');
1541 if ($sel) $isSelFlag++;
1542 $opt[] = '<option value="'.htmlspecialchars($k).'"'.$sel.'>'.htmlspecialchars($v).'</option>';
1543 }
1544
1545 return '<select name="'.$name.'">'.implode('',$opt).'</select>';
1546 }
1547 }
1548
1549 /**
1550 * Used to make the link for the result-browser.
1551 * Notice how the links must resubmit the form after setting the new pointer-value in a hidden formfield.
1552 *
1553 * @param string String to wrap in <a> tag
1554 * @param integer Pointer value
1555 * @return string Input string wrapped in <a> tag with onclick event attribute set.
1556 */
1557 function makePointerSelector_link($str,$p) {
1558 $onclick = 'document.'.$this->prefixId.'[\''.$this->prefixId.'[pointer]\'].value=\''.$p.'\';document.'.$this->prefixId.'.submit();return false;';
1559 return '<a href="#" onclick="'.htmlspecialchars($onclick).'">'.$str.'</a>';
1560 }
1561
1562 /**
1563 * Return icon for file extension
1564 *
1565 * @param string File extension / item type
1566 * @param string Title attribute value in icon.
1567 * @param array TypoScript configuration specifically for search result.
1568 * @return string <img> tag for icon
1569 */
1570 function makeItemTypeIcon($it,$alt='',$specRowConf) {
1571 if (!isset($this->iconFileNameCache[$it])) {
1572 $this->iconFileNameCache[$it] = '';
1573
1574 // If TypoScript is used to render the icon:
1575 if (is_array($this->conf['iconRendering.'])) {
1576 $this->cObj->setCurrentVal($it);
1577 $this->iconFileNameCache[$it] = $this->cObj->cObjGetSingle($this->conf['iconRendering'],$this->conf['iconRendering.']);
1578 } else { // ... otherwise, get flag from sys_language record:
1579
1580 // Default creation / finding of icon:
1581 $icon = '';
1582 if ($it==='0') {
1583 if (is_array($specRowConf['pageIcon.'])) {
1584 $this->iconFileNameCache[$it] = $this->cObj->IMAGE($specRowConf['pageIcon.']);
1585 } else {
1586 $icon = 'EXT:indexed_search/pi/res/pages.gif';
1587 }
1588 } elseif ($this->external_parsers[$it]) {
1589 $icon = $this->external_parsers[$it]->getIcon($it);
1590 }
1591
1592 if ($icon) {
1593 $fullPath = t3lib_div::getFileAbsFileName($icon);
1594
1595 if ($fullPath) {
1596 $info = @getimagesize($fullPath);
1597 $iconPath = substr($fullPath,strlen(PATH_site));
1598 $this->iconFileNameCache[$it] = is_array($info) ? '<img src="'.$iconPath.'" '.$info[3].' title="'.htmlspecialchars($alt).'" alt="" />' : '';
1599 }
1600 }
1601 }
1602 }
1603 return $this->iconFileNameCache[$it];
1604 }
1605
1606 /**
1607 * Return the rating-HTML code for the result row. This makes use of the $this->firstRow
1608 *
1609 * @param array Result row array
1610 * @return string String showing ranking value
1611 */
1612 function makeRating($row) {
1613
1614 switch((string)$this->piVars['order']) {
1615 case 'rank_count': // Number of occurencies on page
1616 return $row['order_val'].' '.$this->pi_getLL('maketitle_matches');
1617 break;
1618 case 'rank_first': // Close to top of page
1619 return ceil(t3lib_div::intInRange(255-$row['order_val'],1,255)/255*100).'%';
1620 break;
1621 case 'rank_flag': // Based on priority assigned to <title> / <meta-keywords> / <meta-description> / <body>
1622 if ($this->firstRow['order_val2']) {
1623 $base = $row['order_val1']*256; // (3 MSB bit, 224 is highest value of order_val1 currently)
1624 $freqNumber = $row['order_val2']/$this->firstRow['order_val2']*pow(2,13); // 16-3 MSB = 13
1625 $total = t3lib_div::intInRange($base+$freqNumber,0,65536);
1626 #debug($total);
1627 #debug(log($total)/log(65536)*100,1);
1628 return ceil(log($total)/log(65536)*100).'%';
1629 }
1630 break;
1631 case 'rank_freq': // Based on frequency
1632 $max = 10000;
1633 $total = t3lib_div::intInRange($row['order_val'],0,$max);
1634 # debug($total);
1635 return ceil(log($total)/log($max)*100).'%';
1636 break;
1637 case 'crdate': // Based on creation date
1638 return $this->cObj->calcAge(time()-$row['item_crdate'],0); // ,$conf['age']
1639 break;
1640 case 'mtime': // Based on modification time
1641 return $this->cObj->calcAge(time()-$row['item_mtime'],0); // ,$conf['age']
1642 break;
1643 default: // fx. title
1644 return '&nbsp;';
1645 break;
1646 }
1647 }
1648
1649 /**
1650 * Returns the resume for the search-result.
1651 *
1652 * @param array Search result row
1653 * @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.
1654 * @param integer String length
1655 * @return string HTML string ...
1656 */
1657 function makeDescription($row,$noMarkup=0,$lgd=180) {
1658 if ($row['show_resume']) {
1659 if (!$noMarkup) {
1660 $markedSW = '';
1661 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'index_fulltext', 'phash='.intval($row['phash']));
1662 if ($ftdrow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
1663 $markedSW = $this->markupSWpartsOfString($ftdrow['fulltextdata']);
1664 }
1665 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1666 }
1667
1668 if (trim($markedSW)) {
1669 return $this->utf8_to_currentCharset($markedSW);
1670 } else {
1671 $outputStr = $GLOBALS['TSFE']->csConvObj->crop('utf-8',$row['item_description'],$lgd);
1672 $outputStr = htmlspecialchars($outputStr);
1673
1674 return $this->utf8_to_currentCharset($outputStr);
1675 }
1676 } else {
1677 return '<span class="noResume">'.$this->pi_getLL('res_noResume','',1).'</span>';
1678 }
1679 }
1680
1681 /**
1682 * Marks up the search words from $this->sWarr in the $str with a color.
1683 *
1684 * @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.
1685 * @return string Processed content.
1686 */
1687 function markupSWpartsOfString($str) {
1688
1689 // Init:
1690 $str = str_replace('&nbsp;',' ',t3lib_parsehtml::bidir_htmlspecialchars($str,-1));
1691 $str = ereg_replace('[[:space:]]+',' ',$str);
1692 $swForReg = array();
1693
1694 // Prepare search words for regex:
1695 foreach($this->sWArr as $d) {
1696 $swForReg[] = quotemeta($d['sword']);
1697 }
1698 $regExString = '('.implode('|',$swForReg).')';
1699
1700 // Split and combine:
1701 $parts = preg_split('/'.$regExString.'/i', ' '.$str.' ', 20000, PREG_SPLIT_DELIM_CAPTURE);
1702 #debug($parts,$regExString);
1703 // Constants:
1704 $summaryMax = 300;
1705 $postPreLgd = 60;
1706 $postPreLgd_offset = 5;
1707 $divider = ' ... ';
1708
1709 $occurencies = (count($parts)-1)/2;
1710 if ($occurencies) {
1711 $postPreLgd = t3lib_div::intInRange($summaryMax/$occurencies,$postPreLgd,$summaryMax/2);
1712 }
1713
1714 // Variable:
1715 $summaryLgd = 0;
1716 $output = array();
1717
1718 // Shorten in-between strings:
1719 foreach($parts as $k => $strP) {
1720 if (($k%2)==0) {
1721
1722 // Find length of the summary part:
1723 $strLen = $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $parts[$k]);
1724 $output[$k] = $parts[$k];
1725
1726 // Possibly shorten string:
1727 if (!$k) { // First entry at all (only cropped on the frontside)
1728 if ($strLen > $postPreLgd) {
1729 $output[$k] = $divider.ereg_replace('^[^[:space:]]+[[:space:]]','',$GLOBALS['TSFE']->csConvObj->crop('utf-8',$parts[$k],-($postPreLgd-$postPreLgd_offset)));
1730 }
1731 } elseif ($summaryLgd > $summaryMax || !isset($parts[$k+1])) { // In case summary length is exceed OR if there are no more entries at all:
1732 if ($strLen > $postPreLgd) {
1733 $output[$k] = ereg_replace('[[:space:]][^[:space:]]+$','',$GLOBALS['TSFE']->csConvObj->crop('utf-8',$parts[$k],$postPreLgd-$postPreLgd_offset)).$divider;
1734 }
1735 } else { // In-between search words:
1736 if ($strLen > $postPreLgd*2) {
1737 $output[$k] = ereg_replace('[[:space:]][^[:space:]]+$','',$GLOBALS['TSFE']->csConvObj->crop('utf-8',$parts[$k],$postPreLgd-$postPreLgd_offset)).
1738 $divider.
1739 ereg_replace('^[^[:space:]]+[[:space:]]','',$GLOBALS['TSFE']->csConvObj->crop('utf-8',$parts[$k],-($postPreLgd-$postPreLgd_offset)));
1740 }
1741 }
1742 $summaryLgd+= $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $output[$k]);;
1743
1744 // Protect output:
1745 $output[$k] = htmlspecialchars($output[$k]);
1746
1747 // If summary lgd is exceed, break the process:
1748 if ($summaryLgd > $summaryMax) {
1749 break;
1750 }
1751 } else {
1752 $summaryLgd+= $GLOBALS['TSFE']->csConvObj->strlen('utf-8',$strP);
1753 $output[$k] = '<span class="tx-indexedsearch-redMarkup">'.htmlspecialchars($parts[$k]).'</span>';
1754 }
1755 }
1756
1757 // Return result:
1758 return implode('',$output);
1759 }
1760
1761 /**
1762 * Returns the title of the search result row
1763 *
1764 * @param array Result row
1765 * @return string Title from row
1766 */
1767 function makeTitle($row) {
1768 $add = '';
1769
1770 if ($this->multiplePagesType($row['item_type'])) {
1771 $dat = unserialize($row['cHashParams']);
1772
1773 $pp = explode('-',$dat['key']);
1774 if ($pp[0]!=$pp[1]) {
1775 $add=', '.$this->pi_getLL('word_pages').' '.$dat['key'];
1776 } else $add=', '.$this->pi_getLL('word_page').' '.$pp[0];
1777 }
1778
1779 $outputString = $GLOBALS['TSFE']->csConvObj->crop('utf-8',$row['item_title'],50,'...');
1780
1781 return $this->utf8_to_currentCharset(htmlspecialchars($outputString)).$add;
1782 }
1783
1784 /**
1785 * Returns the info-string in the bottom of the result-row display (size, dates, path)
1786 *
1787 * @param array Result row
1788 * @param array Template array to modify
1789 * @return array Modified template array
1790 */
1791 function makeInfo($row,$tmplArray) {
1792 $tmplArray['size'] = $this->pi_getLL('res_size','',1).' '.t3lib_div::formatSize($row['item_size']).'';
1793 $tmplArray['created'] = $this->pi_getLL('res_created','',1).' '.date('d-m-y',$row['item_crdate']).'';
1794 $tmplArray['modified'] = $this->pi_getLL('res_modified','',1).' '.date('d-m-y H:i',$row['item_mtime']).'';
1795
1796 $pathId = $row['data_page_id']?$row['data_page_id']:$row['page_id'];
1797 $pathMP = $row['data_page_id']?$row['data_page_mp']:'';
1798
1799 $pI = parse_url($row['data_filename']);
1800 if ($pI['scheme']) {
1801 $tmplArray['path'] = '<a href="'.htmlspecialchars($row['data_filename']).'">'.htmlspecialchars($row['data_filename']).'</a>';
1802 } else {
1803 $pathStr = htmlspecialchars($this->getPathFromPageId($pathId,$pathMP));
1804 $tmplArray['path'] = $this->linkPage($pathId,htmlspecialchars($pathStr),array('data_page_mp'=>$pathMP));
1805 }
1806
1807 return $tmplArray;
1808 }
1809
1810 /**
1811 * Returns configuration from TypoScript for result row based on ID / location in page tree!
1812 *
1813 * @param array Result row
1814 * @return array Configuration array
1815 */
1816 function getSpecialConfigForRow($row) {
1817 $pathId = $row['data_page_id'] ? $row['data_page_id'] : $row['page_id'];
1818 $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
1819
1820 $rl = $this->getRootLine($pathId,$pathMP);
1821 $specConf = $this->conf['specConfs.']['0.'];
1822 if (is_array($rl)) {
1823 foreach($rl as $dat) {
1824 if (is_array($this->conf['specConfs.'][$dat['uid'].'.'])) {
1825 $specConf = $this->conf['specConfs.'][$dat['uid'].'.'];
1826 break;
1827 }
1828 }
1829 }
1830
1831 return $specConf;
1832 }
1833
1834 /**
1835 * Returns the HTML code for language indication.
1836 *
1837 * @param array Result row
1838 * @return string HTML code for result row.
1839 */
1840 function makeLanguageIndication($row) {
1841
1842 // If search result is a TYPO3 page:
1843 if ((string)$row['item_type']==='0') {
1844
1845 // If TypoScript is used to render the flag:
1846 if (is_array($this->conf['flagRendering.'])) {
1847 $this->cObj->setCurrentVal($row['sys_language_uid']);
1848 return $this->cObj->cObjGetSingle($this->conf['flagRendering'],$this->conf['flagRendering.']);
1849 } else { // ... otherwise, get flag from sys_language record:
1850
1851 // Get sys_language record
1852 $rowDat = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'sys_language', 'uid='.intval($row['sys_language_uid']).' '.$this->cObj->enableFields('sys_language'));
1853
1854 // Flag code:
1855 $flag = $rowDat[0]['flag'];
1856 if ($flag) {
1857 $file = 't3lib/gfx/flags/'.$flag;
1858 $imgInfo = @getimagesize(PATH_site.$file);
1859
1860 if (is_array($imgInfo)) {
1861 $output = '<img src="'.$file.'" '.$imgInfo[3].' title="'.htmlspecialchars($rowDat[0]['title']).'" alt="'.htmlspecialchars($rowDat[0]['title']).'" />';
1862 return $output;
1863 }
1864 }
1865 }
1866 }
1867 return '&nbsp;';
1868 }
1869
1870 /**
1871 * Returns the HTML code for the locking symbol.
1872 * NOTICE: Requires a call to ->getPathFromPageId() first in order to work (done in ->makeInfo() by calling that first)
1873 *
1874 * @param integer Page id for which to find answer
1875 * @return string <img> tag if access is limited.
1876 */
1877 function makeAccessIndication($id) {
1878 if (is_array($this->fe_groups_required[$id]) && count($this->fe_groups_required[$id])) {
1879 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="" />';
1880 }
1881 }
1882
1883 /**
1884 * Links the $str to page $id
1885 *
1886 * @param integer Page id
1887 * @param string Title String to link
1888 * @param array Result row
1889 * @return string <A> tag wrapped title string.
1890 */
1891 function linkPage($id,$str,$row=array()) {
1892
1893 // Parameters for link:
1894 $urlParameters = unserialize($row['cHashParams']);
1895
1896 // Add &type and &MP variable:
1897 if ($row['data_page_type']) $urlParameters['type'] = $row['data_page_type'];
1898 if ($row['data_page_mp']) $urlParameters['MP'] = $row['data_page_mp'];
1899
1900 // 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...
1901 if (!is_array($this->domain_records[$id])) {
1902 $this->getPathFromPageId($id);
1903 }
1904
1905 // If external domain, then link to that:
1906 if (count($this->domain_records[$id])) {
1907 reset($this->domain_records[$id]);
1908 $firstDom = current($this->domain_records[$id]);
1909 $scheme = t3lib_div::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://';
1910
1911 $addParams = '';
1912 if (is_array($urlParameters)) {
1913 if (count($urlParameters)) {
1914 reset($urlParameters);
1915 while(list($k,$v)=each($urlParameters)) {
1916 $addParams.= '&'.$k.'='.rawurlencode($v);
1917 }
1918 }
1919 }
1920
1921 return '<a href="'.$scheme.$firstDom.'/index.php?id='.$id.$addParams.'" target="'.$this->conf['search.']['detect_sys_domain_records.']['target'].'">'.htmlspecialchars($str).'</a>';
1922 } else {
1923 return $this->pi_linkToPage($str,$id,$this->conf['result_link_target'],$urlParameters);
1924 }
1925 }
1926
1927 /**
1928 * Returns the path to the page $id
1929 *
1930 * @param integer Page ID
1931 * @param string MP variable content.
1932 * @return string Root line for result.
1933 */
1934 function getRootLine($id,$pathMP='') {
1935 $identStr = $id.'|'.$pathMP;
1936
1937 if (!isset($this->cache_path[$identStr])) {
1938 $this->cache_rl[$identStr] = $GLOBALS['TSFE']->sys_page->getRootLine($id,$pathMP);
1939 }
1940 return $this->cache_rl[$identStr];
1941 }
1942
1943 /**
1944 * Gets the first sys_domain record for the page, $id
1945 *
1946 * @param integer Page id
1947 * @return string Domain name
1948 */
1949 function getFirstSysDomainRecordForPage($id) {
1950 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('domainName', 'sys_domain', 'pid='.intval($id).$this->cObj->enableFields('sys_domain'), '', 'sorting');
1951 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
1952 return ereg_replace('\/$','',$row['domainName']);
1953 }
1954
1955 /**
1956 * Returns the path to the page $id
1957 *
1958 * @param integer Page ID
1959 * @param string MP variable content
1960 * @return string Path
1961 */
1962 function getPathFromPageId($id,$pathMP='') {
1963
1964 $identStr = $id.'|'.$pathMP;
1965
1966 if (!isset($this->cache_path[$identStr])) {
1967 $this->fe_groups_required[$id] = array();
1968 $this->domain_records[$id] = array();
1969 $rl = $this->getRootLine($id,$pathMP);
1970 $hitRoot = 0;
1971 $path = '';
1972 if (is_array($rl) && count($rl)) {
1973 reset($rl);
1974 while(list($k,$v)=each($rl)) {
1975 // Check fe_user
1976 if ($v['fe_group'] && ($v['uid']==$id || $v['extendToSubpages'])) {
1977 $this->fe_groups_required[$id][]=$v['fe_group'];
1978 }
1979 // Check sys_domain.
1980 if ($this->conf['search.']['detect_sys_domain_records']) {
1981 $sysDName = $this->getFirstSysDomainRecordForPage($v['uid']);
1982 if ($sysDName) {
1983 $this->domain_records[$id][] = $sysDName;
1984 // Set path accordingly:
1985 $path = $sysDName.$path;
1986 break;
1987 }
1988 }
1989
1990 // Stop, if we find that the current id is the current root page.
1991 if ($v['uid']==$GLOBALS['TSFE']->config['rootLine'][0]['uid']) {
1992 break;
1993 }
1994 $path = '/'.$v['title'].$path;
1995 }
1996 }
1997
1998 $this->cache_path[$identStr] = $path;
1999
2000 if (is_array($this->conf['path_stdWrap.'])) {
2001 $this->cache_path[$identStr] = $this->cObj->stdWrap($this->cache_path[$identStr], $this->conf['path_stdWrap.']);
2002 }
2003 }
2004
2005 return $this->cache_path[$identStr];
2006 }
2007
2008 /**
2009 * Return the menu of pages used for the selector.
2010 *
2011 * @param integer Page ID for which to return menu
2012 * @return array Menu items (for making the section selector box)
2013 */
2014 function getMenu($id) {
2015 if ($this->conf['show.']['LxALLtypes']) {
2016 $output = Array();
2017 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('title,uid', 'pages', 'pid='.intval($id).$this->cObj->enableFields('pages'), '', 'sorting');
2018 while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
2019 $output[$row['uid']] = $GLOBALS['TSFE']->sys_page->getPageOverlay($row);
2020 }
2021 return $output;
2022 } else {
2023 return $GLOBALS['TSFE']->sys_page->getMenu($id);
2024 }
2025 }
2026
2027 /**
2028 * Returns if an item type is a multipage item type
2029 *
2030 * @param string Item type
2031 * @return boolean True if multipage capable
2032 */
2033 function multiplePagesType($item_type) {
2034 return is_object($this->external_parsers[$item_type]) && $this->external_parsers[$item_type]->isMultiplePageExtension($item_type);
2035 }
2036
2037 /**
2038 * Converts the input string from utf-8 to the backend charset.
2039 *
2040 * @param string String to convert (utf-8)
2041 * @return string Converted string (backend charset if different from utf-8)
2042 */
2043 function utf8_to_currentCharset($str) {
2044 return $GLOBALS['TSFE']->csConv($str,'utf-8');
2045 }
2046
2047 /**
2048 * Returns an object reference to the hook object if any
2049 *
2050 * @param string Name of the function you want to call / hook key
2051 * @return object Hook object, if any. Otherwise null.
2052 */
2053 function &hookRequest($functionName) {
2054 global $TYPO3_CONF_VARS;
2055
2056 // Hook: menuConfig_preProcessModMenu
2057 if ($TYPO3_CONF_VARS['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
2058 $hookObj = &t3lib_div::getUserObj($TYPO3_CONF_VARS['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
2059 if (method_exists ($hookObj, $functionName)) {
2060 $hookObj->pObj = &$this;
2061 return $hookObj;
2062 }
2063 }
2064 }
2065 }
2066
2067
2068 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/indexed_search/pi/class.tx_indexedsearch.php']) {
2069 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/indexed_search/pi/class.tx_indexedsearch.php']);
2070 }
2071 ?>