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