162db1b38e2f5b5b3ca7ef3d4ba950039a3afae7
[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
40
41
42 require_once(PATH_tslib."class.tslib_pibase.php");
43 require_once(PATH_tslib."class.tslib_search.php");
44 require_once(t3lib_extMgm::extPath("indexed_search")."class.indexer.php");
45
46 class tx_indexedsearch extends tslib_pibase {
47 var $prefixId = "tx_indexedsearch"; // Same as class name
48 var $scriptRelPath = "pi/class.tx_indexedsearch.php"; // Path to this script relative to the extension dir.
49 var $extKey = "indexed_search"; // The extension key.
50 var $join_pages=0; // See document for info about this flag...
51
52 var $defaultResultNumber=20;
53 var $wholeSiteIdList = 0;
54
55 var $operator_translate_table = Array ( // case-sensitiv. Defineres the words, which will be operators between words
56 Array ("+" , "AND"),
57 Array ("|" , "OR"),
58 Array ("-" , "AND NOT"),
59 // english
60 # Array ("AND" , "AND"),
61 # Array ("OR" , "OR"),
62 # Array ("NOT" , "AND NOT"),
63 );
64
65 // Internals:
66 var $cache_path=array();
67 var $cache_rl=array();
68 var $fe_groups_required=array();
69 var $domain_records=array();
70 var $sWArr=array();
71 var $wSelClauses=array();
72 var $firstRow=array();
73 var $resultSections=array();
74
75 var $anchorPrefix = ''; // Prefix for local anchors. For "speaking URLs" to work with <base>-url set.
76
77 function main($content,$conf) {
78 $this->conf=$conf;
79 $this->pi_loadLL();
80 $this->pi_setPiVarDefaults();
81 $this->anchorPrefix = substr(t3lib_div::getIndpEnv('TYPO3_REQUEST_URL'),strlen(t3lib_div::getIndpEnv('TYPO3_SITE_URL')));
82
83 #debug($this->piVars);
84
85 // Initialize the indexer-class - just to use a few function (for making hashes)
86 $this->indexerObj = t3lib_div::makeInstance("tx_indexedsearch_indexer");
87
88 // If "_sections" is set, this value overrides any existing value.
89 if ($this->piVars["_sections"]) $this->piVars["sections"] = $this->piVars["_sections"];
90
91 // Add previous search words to current
92 if ($this->piVars['sword_prev_include'] && $this->piVars["sword_prev"]) {
93 $this->piVars["sword"] = trim($this->piVars["sword_prev"]).' '.$this->piVars["sword"];
94 }
95
96 // Selector-box values defined here:
97 $optValues = Array(
98 "type" => Array(
99 "0" => $this->pi_getLL("opt_type_0"),
100 "1" => $this->pi_getLL("opt_type_1"),
101 "2" => $this->pi_getLL("opt_type_2"),
102 "3" => $this->pi_getLL("opt_type_3"),
103 "10" => $this->pi_getLL("opt_type_10"),
104 "20" => $this->pi_getLL("opt_type_20"),
105 ),
106 "defOp" => Array(
107 "0" => $this->pi_getLL("opt_defOp_0"),
108 "1" => $this->pi_getLL("opt_defOp_1"),
109 ),
110 "sections" => Array(
111 "0" => $this->pi_getLL("opt_sections_0"),
112 "-1" => $this->pi_getLL("opt_sections_-1"),
113 "-2" => $this->pi_getLL("opt_sections_-2"),
114 "-3" => $this->pi_getLL("opt_sections_-3"),
115 // 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.
116 ),
117 "media" => Array(
118 "-1" => $this->pi_getLL("opt_media_-1"),
119 "0" => $this->pi_getLL("opt_media_0"),
120 "-2" => $this->pi_getLL("opt_media_-2"),
121 "1" => $this->pi_getLL("opt_media_1"),
122 "2" => $this->pi_getLL("opt_media_2"),
123 "3" => $this->pi_getLL("opt_media_3"),
124 ),
125 "order" => Array(
126 "rank_flag" => $this->pi_getLL("opt_order_rank_flag"),
127 "rank_freq" => $this->pi_getLL("opt_order_rank_freq"),
128 "rank_first" => $this->pi_getLL("opt_order_rank_first"),
129 "rank_count" => $this->pi_getLL("opt_order_rank_count"),
130 "mtime" => $this->pi_getLL("opt_order_mtime"),
131 "title" => $this->pi_getLL("opt_order_title"),
132 "crdate" => $this->pi_getLL("opt_order_crdate"),
133 # "rating" => "Page-rating",
134 # "hits" => "Page-hits",
135 ),
136 "group" => Array (
137 "sections" => $this->pi_getLL("opt_group_sections"),
138 "flat" => $this->pi_getLL("opt_group_flat"),
139 ),
140 "lang" => Array (
141 -1 => $this->pi_getLL("opt_lang_-1"),
142 0 => $this->pi_getLL("opt_lang_0"),
143 ),
144 "desc" => Array (
145 "0" => $this->pi_getLL("opt_desc_0"),
146 "1" => $this->pi_getLL("opt_desc_1"),
147 ),
148 "results" => Array (
149 "10" => "10",
150 "20" => "20",
151 "50" => "50",
152 "100" => "100",
153 )
154 );
155
156 $this->operator_translate_table[]=Array ($this->pi_getLL("local_operator_AND") , "AND");
157 $this->operator_translate_table[]=Array ($this->pi_getLL("local_operator_OR") , "OR");
158 $this->operator_translate_table[]=Array ($this->pi_getLL("local_operator_NOT") , "AND NOT");
159
160 // This is the id of the site root. This value may be a commalist of integer (prepared for this)
161 $this->wholeSiteIdList=intval($GLOBALS["TSFE"]->config["rootLine"][0]["uid"]);
162
163 // This selects the first and secondary menus for the "sections" selector - so we can search in sections and sub sections.
164 if ($this->conf["show."]["L1sections"]) {
165 $firstLevelMenu = $this->getMenu($this->wholeSiteIdList);
166 # debug($firstLevelMenu);
167 while(list($kk,$mR)=each($firstLevelMenu)) {
168 if ($mR["doktype"]!=5) {
169 $optValues["sections"]["rl1_".$mR["uid"]]=trim($this->pi_getLL("opt_RL1")." ".$mR["title"]);
170 if ($this->conf["show."]["L2sections"]) {
171 $secondLevelMenu = $this->getMenu($mR["uid"]);
172 while(list($kk2,$mR2)=each($secondLevelMenu)) {
173 if ($mR["doktype"]!=5) {
174 $optValues["sections"]["rl2_".$mR2["uid"]]=trim($this->pi_getLL("opt_RL2")." ".$mR2["title"]);
175 } else unset($secondLevelMenu[$kk2]);
176 }
177 $optValues["sections"]["rl2_".implode(",",array_keys($secondLevelMenu))]=$this->pi_getLL("opt_RL2ALL");
178 }
179 } else unset($firstLevelMenu[$kk]);
180 }
181 $optValues["sections"]["rl1_".implode(",",array_keys($firstLevelMenu))]=$this->pi_getLL("opt_RL1ALL");
182 }
183
184 // 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...
185 if ($this->conf["search."]["rootPidList"]) {
186 $this->wholeSiteIdList = implode(",",t3lib_div::intExplode(",",$this->conf["search."]["rootPidList"]));
187 #debug($this->wholeSiteIdList);
188 }
189
190
191 // Add search languages:
192 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'sys_language', '1=1'.$this->cObj->enableFields('sys_language'));
193 while($lR = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
194 $optValues["lang"][$lR["uid"]]=$lR["title"];
195 }
196
197
198
199 #debug($this->piVars);
200 // Setting first values in optValues as default values IF there is not corresponding piVar value set already.
201 reset($optValues);
202 while(list($kk,$vv)=each($optValues)) {
203 if (!isset($this->piVars[$kk])) {
204 reset($vv);
205 $this->piVars[$kk]=key($vv);
206 }
207 }
208 #debug($this->piVars);
209
210 // Blind selectors:
211 if (is_array($this->conf["blind."])) {
212 reset($this->conf["blind."]);
213 while(list($kk,$vv)=each($this->conf["blind."])) {
214 if (is_array($vv)) {
215 reset($vv);
216 while(list($kkk,$vvv)=each($vv)) {
217 if (!is_array($vvv) && $vvv && is_array($optValues[substr($kk,0,-1)])) {
218 unset($optValues[substr($kk,0,-1)][$kkk]);
219 }
220 }
221 } elseif ($vv) { // If value is not set, unset the option array.
222 unset($optValues[$kk]);
223 }
224 }
225 }
226
227
228 // This gets the search-words into the $sWArr:
229 $this->sWArr = $sWArr = $this->getSearchWords($this->piVars["defOp"]);
230 #debug($this->sWArr);
231
232 // If there was any search words entered...
233 if (is_array($sWArr)) {
234 $content = $this->doSearch($sWArr);
235 } // END: There was a search word.
236
237 // Finally compile all the content, form, messages and results:
238 $content=
239 $this->makeSearchForm($optValues).
240 $this->printRules().
241 $content;
242
243 return $this->pi_wrapInBaseClass($content);
244 }
245
246 /**
247 * Performs the search, if any search words found.
248 */
249 function doSearch($sWArr) {
250 $rowcontent="";
251 $pt1=t3lib_div::milliseconds();
252
253 // This SEARCHES for the searchwords in $sWArr AND returns a COMPLETE list of phash-integers of the matches.
254 $list = $this->getPhashList($sWArr);
255 $pt2=t3lib_div::milliseconds();
256
257
258 // IF there were any matches (there is results...) then go on.
259 if ($list) {
260 $GLOBALS["TT"]->push("Searching Final result");
261
262 // Do the search:
263 $res = $this->execFinalQuery($list);
264
265 // Get some variables:
266 $count = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
267 #debug($count);
268 $this->piVars["results"] = $displayCount = t3lib_div::intInRange($this->piVars["results"],1,100000,$this->defaultResultNumber);
269 $pointer=t3lib_div::intInRange($this->piVars["pointer"],0,floor($count/$displayCount));
270
271 $pt3=t3lib_div::milliseconds();
272
273 // Now, traverse result and put the rows to be displayed into an array
274 $lines=Array();
275 $c=0;
276 $this->firstRow=Array(); // Will hold the first row in result - used to calculate relative hit-ratings.
277 $this->resultRows=Array(); // Will hold the results rows for display.
278 $this->grouping_phashes=array(); // Used to filter out duplicates.
279 $this->grouping_chashes=array(); // Used to filter out duplicates BASED ON cHash.
280 while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
281 if (!$c) {
282 $this->firstRow=$row;
283 }
284
285 $row["show_resume"]=$this->checkResume($row);
286 $phashGr = !in_array($row["phash_grouping"],$this->grouping_phashes);
287 $chashGr = !in_array($row["contentHash"].".".$row["data_page_id"],$this->grouping_chashes);
288 if ($phashGr && $chashGr) {
289 if ($row["show_resume"]) { // Only if the resume may be shown are we going to filter out duplicates...
290 if ($row["item_type"]!=2) { // Only on documents which are not PDF files.
291 $this->grouping_phashes[]=$row["phash_grouping"];
292 }
293 $this->grouping_chashes[]=$row["contentHash"].".".$row["data_page_id"];
294 }
295 $c++;
296
297 // All rows for display is put into resultRows[]
298 if ($c > $pointer*$displayCount) {
299 $row["result_number"]=$c;
300 $this->resultRows[] = $row;
301 if ($c+1 > ($pointer+1)*$displayCount) break;
302 }
303 } else {
304 $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.
305 # debug();
306 }
307 }
308 $GLOBALS["TT"]->pull();
309
310 #debug($this->resultRows);
311 #debug(count($this->resultRows));
312 #debug($this->grouping_chashes);
313
314 $GLOBALS["TT"]->push("Display Final result");
315
316 // SO, on to the result display here:
317 $rowcontent.=$this->compileResult($this->resultRows);
318 $pt4=t3lib_div::milliseconds();
319
320 // Makes the result browsing navigation (next/prev, 1-2-3)
321 # $PS = $this->makePointerSelector($count,$displayCount,$pointer);
322
323
324 // Browsing box:
325 if ($count) {
326 #$content.=$PS."<HR>";
327 $this->internal["res_count"]=$count;
328 $this->internal["results_at_a_time"]=$displayCount;
329 $this->internal["maxPages"]=t3lib_div::intInRange($this->conf["search."]["page_links"],1,100,10);
330 $addString = ($count&&$this->piVars["group"]=="sections"?" ".sprintf($this->pi_getLL("inNsection".(count($this->resultSections)>1?"s":"")),count($this->resultSections)):"");
331 $browseBox1 = $this->pi_list_browseresults(1,$addString,$this->printResultSectionLinks());
332 $browseBox2 = $this->pi_list_browseresults(0);
333 }
334
335 // Print the time the search took:
336 if ($pt1 && $this->conf["show."]["parsetimes"]) {
337 $parsetimes="";
338 $parsetimes.="<p>Word Search took: ".($pt2-$pt1)." ms<BR>";
339 $parsetimes.="Order Search took: ".($pt3-$pt2)." ms<BR>";
340 $parsetimes.="Display took: ".($pt4-$pt3)." ms</p><HR>";
341 }
342
343 // Browsing nav, bottom.
344 if ($count) {
345 $content=$browseBox1.$rowcontent.$browseBox2;
346 } else {
347 $content='<p'.$this->pi_classParam("noresults").'>'.$this->pi_getLL("noResults").'</p>';
348 }
349 $content.=$parsetimes;
350
351 $GLOBALS["TT"]->pull();
352 } else { // No results found:
353 $content.='<p'.$this->pi_classParam("noresults").'>'.$this->pi_getLL("noResults").'</p>';
354 }
355
356 // Print a message telling which words we searched for, and in which sections etc.
357 $what=$this->tellUsWhatIsSeachedFor($sWArr).
358 (substr($this->piVars["sections"],0,2)=="rl"?" ".$this->pi_getLL("inSection")." '".substr($this->getPathFromPageId(substr($this->piVars["sections"],4)),1)."'":"");
359 $what='<div'.$this->pi_classParam("whatis").'><p>'.$what.'</p></div>';
360 $content=$what.$content;
361
362 // Write search statistics
363 $this->writeSearchStat($sWArr,$count,array($pt1,$pt2,$pt3,$pt4));
364 return $content;
365 }
366
367
368
369
370
371
372 /***********************************
373
374 SEARCHING FUNCTIONS
375
376 ***********************************/
377
378
379
380
381
382
383
384 /**
385 * This splits the search word input into an array where each word is
386 *
387 * Only words with 2 or more characters are accepted
388 * Max 200 chars total
389 * Space is used to split words, "" can be used search for a whole string (not indexed search then)
390 * AND, OR and NOT are prefix words, overruling the default operator
391 * +/|/- equals AND, OR and NOT as operators.
392 * All search words are converted to lowercase.
393 *
394 * $defOp is the default operator. 1=OR, 0=AND
395 */
396 function getSearchWords($defOp) {
397 $inSW = substr($this->piVars["sword"],0,200);
398
399 // Convert to UTF-8:
400 $inSW = $GLOBALS['TSFE']->csConvObj->utf8_encode($inSW, $GLOBALS['TSFE']->metaCharset);
401
402 if ($this->piVars["type"]==20) {
403 return array(array("sword"=>trim($inSW),"oper"=>"AND"));
404 } else {
405 $search = t3lib_div::makeInstance("tslib_search");
406 $search->default_operator = $defOp==1 ? 'OR' : 'AND';
407 $search->operator_translate_table = $this->operator_translate_table;
408 $search->register_and_explode_search_string($inSW);
409
410 if (is_array($search->sword_array)) {
411 return $search->sword_array;
412 }
413 }
414 }
415
416 /**
417 * Returns a COMPLETE list of phash-integers matching the search-result composed of the search-words in the sWArr array.
418 * The list of phash integers are unsorted and should be used for subsequent selection of index_phash records for display of the result.
419 */
420 function getPhashList($sWArr) {
421 $c=0;
422
423 $totalHashList=array(); // This array accumulates the phash-values
424 $this->wSelClauses=array();
425
426 reset($sWArr);
427 while(list($k,$v)=each($sWArr)) {
428 $sWord = strtolower($v["sword"]); // lower-case all of them...
429
430 $GLOBALS["TT"]->push("SearchWord ".$sWord);
431
432 $plusQ="";
433 /*
434 // Maybe this will improve the search queries. Tests has shown it not to do so though...
435 if (count($totalHashList)) {
436 switch($v["oper"]) {
437 case "OR":
438 $plusQ = "AND IR.phash NOT IN (".implode(",",$totalHashList).")";
439 break;
440 case "AND NOT":
441 default: // AND
442 $plusQ = "AND IR.phash IN (".implode(",",$totalHashList).")";
443 break;
444 }
445 }
446 $plusQ="";
447 */
448
449 // Making the query for a single search word based on the search-type
450 $res="";
451 $theType = (string)$this->piVars["type"];
452 if (strstr($sWord," ")) $theType=20; // If there are spaces in the search-word, make a full text search instead.
453
454 $wSel="";
455 switch($theType) {
456 case "1":
457 $wSel = "IW.baseword LIKE '%".$GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_words')."%'";
458 $res = $this->execPHashListQuery($wSel,$plusQ);
459
460 break;
461 case "2":
462 $wSel = "IW.baseword LIKE '".$GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_words')."%'";
463 $res = $this->execPHashListQuery($wSel,$plusQ);
464 break;
465 case "3":
466 $wSel = "IW.baseword LIKE '%".$GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_words')."'";
467 $res = $this->execPHashListQuery($wSel,$plusQ);
468 break;
469 case "10":
470 $wSel = "IW.metaphone = ".$this->indexerObj->metaphone($sWord);
471 $res = $this->execPHashListQuery($wSel,$plusQ);
472 break;
473 case "20":
474 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
475 'ISEC.phash',
476 'index_section ISEC, index_fulltext IFT',
477 'IFT.fulltextdata LIKE \'%'.$GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_fulltext').'%\' AND
478 ISEC.phash = IFT.phash
479 '.$this->sectionTableWhere(),
480 'ISEC.phash'
481 );
482 $wSel = "1=1";
483
484 if ($this->piVars["type"]==20) $this->piVars["order"]="mtime"; // If there is a fulltext search for a sentence there is a likelyness 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.
485 break;
486 default:
487 $wSel = "IW.wid = ".$hash = $this->indexerObj->md5inthash($sWord);
488 $res = $this->execPHashListQuery($wSel,$plusQ);
489 break;
490 }
491 $this->wSelClauses[]=$wSel;
492
493 // If there was a query to do, then select all phash-integers which resulted from this.
494 if ($res) {
495
496 # Get phash list by searching for it:
497 $phashList = array();
498 while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
499 $phashList[]=$row["phash"];
500 }
501 $GLOBALS['TYPO3_DB']->sql_free_result($res);
502
503 // Here the phash list are merged with the existing result based on whether we are dealing with OR, NOT or AND operations.
504 if ($c) {
505 switch($v["oper"]) {
506 case "OR":
507 $totalHashList=array_unique(array_merge($phashList,$totalHashList));
508 break;
509 case "AND NOT":
510 $totalHashList=array_diff($totalHashList,$phashList);
511 break;
512 default: // AND...
513 $totalHashList=array_intersect($totalHashList,$phashList);
514 break;
515 }
516 } else {
517 $totalHashList=$phashList; // First search
518 }
519 #debug($totalHashList);
520 }
521
522 $GLOBALS["TT"]->pull();
523 $c++;
524 }
525
526 #debug($sWArr);
527 return implode(",",$totalHashList);
528 }
529
530 /**
531 * Returns a query which selects the search-word from the word/rel tables.
532 */
533 function execPHashListQuery($wordSel,$plusQ="") {
534 return $GLOBALS['TYPO3_DB']->exec_SELECTquery(
535 'IR.phash',
536 'index_words IW,
537 index_rel IR,
538 index_section ISEC',
539 $wordSel.'
540 AND IW.wid=IR.wid
541 AND ISEC.phash = IR.phash
542 '.$this->sectionTableWhere().'
543 '.$plusQ,
544 'IR.phash'
545 );
546 }
547
548 /**
549 * Returns AND statement for selection of section in database. (rootlevel 0-2 + page_id)
550 */
551 function sectionTableWhere() {
552 #debug($this->piVars["sections"]);
553 $out = $this->wholeSiteIdList<0 ? "" : "AND ISEC.rl0 IN (".$this->wholeSiteIdList.")";
554 $list = implode(",",t3lib_div::intExplode(",",substr($this->piVars["sections"],4)));
555
556 if (substr($this->piVars["sections"],0,4)=="rl1_") {
557 $out.= "AND ISEC.rl1 IN (".$list.")";
558 } else if (substr($this->piVars["sections"],0,4)=="rl2_") {
559 $out.= "AND ISEC.rl2 IN (".$list.")";
560 } else {
561 switch((string)$this->piVars["sections"]) {
562 case "-1": // "-1" => "Only this page",
563 $out.= " AND ISEC.page_id=".$GLOBALS["TSFE"]->id;
564 break;
565 case "-2": // "-2" => "Top + level 1",
566 $out.= " AND ISEC.rl2=0";
567 break;
568 case "-3": // "-3" => "Level 2 and out",
569 $out.= " AND ISEC.rl2>0";
570 break;
571 }
572 }
573 return $out;
574 }
575
576 /**
577 * Returns AND statement for selection of media type
578 */
579 function mediaTypeWhere() {
580 switch($this->piVars["media"]) {
581 case 0: // "0" => "Kun TYPO3 sider",
582 $out = "AND IP.item_type=0";
583 break;
584 case 1: // "1" => "Kun HTML dokumenter",
585 $out = "AND IP.item_type=1";
586 break;
587 case 2: // "2" => "Kun PDF dokumenter",
588 $out = "AND IP.item_type=2";
589 break;
590 case 3: // "3" => "Kun Word dokumenter",
591 $out = "AND IP.item_type=3";
592 break;
593 case -2: // All external documents
594 $out = "AND IP.item_type>0";
595 break;
596 case -1:
597 default:
598 $out="";
599 break;
600 }
601 return $out;
602 }
603
604 /**
605 * Returns AND statement for selection of langauge
606 */
607 function languageWhere() {
608 if ($this->piVars["lang"]>=0) { // -1 is the same as ALL language.
609 return "AND IP.sys_language_uid=".intval($this->piVars["lang"]);
610 }
611 }
612
613 /**
614 * Execute final query:
615 */
616 function execFinalQuery($list) {
617
618 $page_join="";
619 $page_where="";
620 if ($this->join_pages) {
621 $page_join = ",
622 pages";
623 $page_where = "pages.uid = ISEC.page_id
624 ".$this->cObj->enableFields("pages")."
625 AND pages.no_search=0
626 AND pages.doktype<200
627 ";
628 } elseif ($this->wholeSiteIdList>=0) {
629 $siteIdNumbers = t3lib_div::intExplode(",",$this->wholeSiteIdList);
630 $id_list=array();
631 while(list(,$rootId)=each($siteIdNumbers)) {
632 $id_list[]=$this->cObj->getTreeList($rootId,9999,0,0,"","").$rootId;
633 }
634 $page_where = "ISEC.page_id IN (".implode(",",$id_list).")";
635 } else {
636 $page_where = " 1=1 ";
637 }
638
639 // 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.
640 if (substr($this->piVars["order"],0,5)=="rank_") {
641 /*
642 OK there were some fancy calculations promoted by Graeme Merrall:
643
644 "However, regarding relevance you probably want to look at something like
645 Salton's formula which is a good easy way to measure relevance.
646 Oracle Intermedia uses this and it's pretty simple:
647 Score can be between 0 and 100, but the top-scoring document in the query
648 will not necessarily have a score of 100 -- scoring is relative, not
649 absolute. This means that scores are not comparable across indexes, or even
650 across different queries on the same index. Score for each document is
651 computed using the standard Salton formula:
652
653 3f(1+log(N/n))
654
655 Where f is the frequency of the search term in the document, N is the total
656 number of rows in the table, and n is the number of rows which contain the
657 search term. This is converted into an integer in the range 0 - 100.
658
659 There's a good doc on it at
660 http://ls6-www.informatik.uni-dortmund.de/bib/fulltext/ir/Pfeifer:97/
661 although it may be a little complex for what you require so just pick the
662 relevant parts out.
663 "
664
665 However I chose not to go with this for several reasons.
666 I do not claim that my ways of calculating importance here is the best.
667 ANY (better) suggestions for ranking calculation is accepted! (as long as they are shipped with tested code in exchange for this.)
668 */
669
670 switch($this->piVars["order"]) {
671 case "rank_flag": // This gives priority to word-position (max-value) so that words in title, keywords, description counts more than in content.
672 // The ordering is refined with the frequency sum as well.
673 $grsel = "MAX(IR.flags) AS order_val1, SUM(IR.freq) AS order_val2";
674 $orderBy = "order_val1".$this->isDescending().",order_val2".$this->isDescending();
675 break;
676 case "rank_first": // Results in average position of search words on page. Must be inversely sorted (low numbers are closer to top)
677 $grsel = "AVG(IR.first) AS order_val";
678 $orderBy = "order_val".$this->isDescending(1);
679 break;
680 case "rank_count": // Number of words found
681 $grsel = "SUM(IR.count) AS order_val";
682 $orderBy = "order_val".$this->isDescending();
683 break;
684 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?
685 $grsel = "SUM(IR.freq) AS order_val";
686 $orderBy = "order_val".$this->isDescending();
687 break;
688 }
689
690 // So, words are imploded into an OR statement (no "sentence search" should be done here - may deselect results)
691 $wordSel='('.implode(' OR ',$this->wSelClauses).') AND ';
692
693 return $GLOBALS['TYPO3_DB']->exec_SELECTquery(
694 'ISEC.*, 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, '
695 .$grsel,
696 'index_words IW,
697 index_rel IR,
698 index_section ISEC,
699 index_phash IP'.
700 $page_join,
701 $wordSel.'
702 IP.phash IN ('.$list.') '.
703 $this->mediaTypeWhere().' '.
704 $this->languageWhere().'
705 AND IW.wid=IR.wid
706 AND ISEC.phash = IR.phash
707 AND IP.phash = IR.phash
708 AND '.$page_where,
709 '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',
710 $orderBy
711 );
712 } else { // Otherwise, if sorting are done with the pages table or other fields, there is no need for joining with the rel/word tables:
713
714 $orderBy = '';
715 switch((string)$this->piVars["order"]) {
716 case "rating":
717 debug("rating: NOT ACTIVE YET");
718 break;
719 case "hits":
720 debug("rating: NOT ACTIVE YET");
721 break;
722 case "title":
723 $orderBy = "IP.item_title".$this->isDescending();
724 break;
725 case "crdate":
726 $orderBy = "IP.item_crdate".$this->isDescending();
727 break;
728 case "mtime":
729 $orderBy = "IP.item_mtime".$this->isDescending();
730 break;
731 }
732
733 return $GLOBALS['TYPO3_DB']->exec_SELECTquery(
734 'ISEC.*, IP.*',
735 'index_phash IP,index_section ISEC'.$page_join,
736 'IP.phash IN ('.$list.') '.
737 $this->mediaTypeWhere().' '.
738 $this->languageWhere().'
739 AND IP.phash = ISEC.phash
740 AND '.$page_where,
741 'IP.phash',
742 $orderBy
743 );
744 }
745 }
746
747 /**
748 * Checking if the resume can be shown for the search result:
749 */
750 function checkResume($row) {
751 if ($row["item_type"]>0) {
752 // phash_t3 is the phash of the parent TYPO3 page row which initiated the indexing of the documents in this section.
753
754 // So, selecting for the grlist records belonging to the parent phash-row where the current users gr_list exists.
755 // 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 this case is rare.
756 $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'));
757 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res)) {
758 # debug("Look up for external media '".$row["data_filename"]."': phash:".$row["phash_t3"]." YES - (".$GLOBALS["TSFE"]->gr_list.")!",1);
759 return 1;
760 } else {
761 # debug("Look up for external media '".$row["data_filename"]."': phash:".$row["phash_t3"]." NO - (".$GLOBALS["TSFE"]->gr_list.")!",1);
762 return 0;
763 }
764 } else { // ALm typo3 pages:
765 if (strcmp($row["gr_list"],$GLOBALS["TSFE"]->gr_list)) {
766 // 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...
767 $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'));
768 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res)) {
769 #debug("Checking on it ...".$row["item_title"]."/".$row["phash"]." - YES (".$GLOBALS["TSFE"]->gr_list.")",1);
770 return 1;
771 } else {
772 #debug("Checking on it ...".$row["item_title"]."/".$row["phash"]." - NOPE",1);
773 return 0;
774 }
775 } else {
776 #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);
777 return 1;
778 }
779 }
780 }
781
782 /**
783 * Returns "DESC" or "" depending on the settings of the incoming highest/lowest result order.
784 */
785 function isDescending($inverse=0) {
786 $desc = $this->piVars["desc"];
787 if ($inverse) $desc=!$desc;
788 return !$desc ? " DESC":"";
789 }
790
791 /**
792 * Write statistics information for the search:
793 */
794 function writeSearchStat($sWArr,$count,$pt) {
795 $insertFields = array(
796 'searchstring' => $this->piVars['sword'],
797 'searchoptions' => serialize(array($this->piVars,$sWArr,$pt)),
798 'feuser_id' => intval($this->fe_user->user['uid']), // fe_user id, integer
799 'cookie' => $this->fe_user->id, // cookie as set or retrieve. If people has cookies disabled this will vary all the time...
800 'IP' => t3lib_div::getIndpEnv('REMOTE_ADDR'), // Remote IP address
801 'hits' => intval($count), // Number of hits on the search.
802 'tstamp' => $GLOBALS['EXEC_TIME'] // Time stamp
803 );
804
805 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_search', $insertFields);
806 $newId = $GLOBALS['TYPO3_DB']->sql_insert_id();
807
808 if ($newId) {
809 foreach($sWArr as $val) {
810 $insertFields = array(
811 'word' => strtolower($val['sword']),
812 'index_stat_search_id' => $newId,
813 'tstamp' => $GLOBALS['EXEC_TIME'] // Time stamp
814 );
815
816 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_word', $insertFields);
817 }
818 }
819 }
820
821
822
823
824
825
826
827
828
829
830
831
832
833 /***********************************
834
835 LAYOUT FUNCTIONS
836
837 ***********************************/
838
839
840
841
842 /**
843 * Make search form
844 */
845 function makeSearchForm($optValues) {
846 $rows=array();
847 // Adding search field and button:
848 $rows[]='<tr>
849 <td nowrap><p>'.$this->pi_getLL("form_searchFor").'&nbsp;</p></td>
850 <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").'"'.$this->pi_classParam("searchbox-button").'></td>
851 </tr>';
852
853 if ($this->conf["show."]["clearSearchBox"] && $this->conf["show."]["clearSearchBox."]['enableSubSearchCheckBox']) {
854 $rows[]='<tr>
855 <td></td>
856 <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"':'').'> Add to current search words</td>
857 </tr>';
858 }
859
860
861 if ($this->piVars["ext"]) {
862 if (is_array($optValues["type"]) || is_array($optValues["defOp"])) $rows[]='<tr>
863 <td nowrap><p>'.$this->pi_getLL("form_match").'&nbsp;</p></td>
864 <td>'.$this->renderSelectBox($this->prefixId.'[type]',$this->piVars["type"],$optValues["type"]).
865 $this->renderSelectBox($this->prefixId.'[defOp]',$this->piVars["defOp"],$optValues["defOp"]).'</td>
866 </tr>';
867 if (is_array($optValues["media"]) || is_array($optValues["lang"])) $rows[]='<tr>
868 <td nowrap><p>'.$this->pi_getLL("form_searchIn").'&nbsp;</p></td>
869 <td>'.$this->renderSelectBox($this->prefixId.'[media]',$this->piVars["media"],$optValues["media"]).
870 $this->renderSelectBox($this->prefixId.'[lang]',$this->piVars["lang"],$optValues["lang"]).'</td>
871 </tr>';
872 if (is_array($optValues["sections"])) $rows[]='<tr>
873 <td nowrap><p>'.$this->pi_getLL("form_fromSection").'&nbsp;</p></td>
874 <td>'.$this->renderSelectBox($this->prefixId.'[sections]',$this->piVars["sections"],$optValues["sections"]).'</td>
875 </tr>';
876 if (is_array($optValues["order"]) || is_array($optValues["desc"]) || is_array($optValues["results"])) $rows[]='<tr>
877 <td nowrap><p>'.$this->pi_getLL("form_orderBy").'&nbsp;</p></td>
878 <td><p>'.$this->renderSelectBox($this->prefixId.'[order]',$this->piVars["order"],$optValues["order"]).
879 $this->renderSelectBox($this->prefixId.'[desc]',$this->piVars["desc"],$optValues["desc"]).
880 $this->renderSelectBox($this->prefixId.'[results]',$this->piVars["results"],$optValues["results"]).'&nbsp;'.$this->pi_getLL("form_atATime").'</p></td>
881 </tr>';
882 if (is_array($optValues["group"]) || !$this->conf["blind."]["extResume"]) $rows[]='<tr>
883 <td nowrap><p>'.$this->pi_getLL("form_style").'&nbsp;</p></td>
884 <td><p>'.$this->renderSelectBox($this->prefixId.'[group]',$this->piVars["group"],$optValues["group"]).
885 (!$this->conf["blind."]["extResume"] ? '&nbsp; &nbsp;
886 <input type="hidden" name="'.$this->prefixId.'[extResume]" value="0"><input type="checkbox" value="1" name="'.$this->prefixId.'[extResume]"'.($this->piVars["extResume"]?" CHECKED":"").'>'.$this->pi_getLL("form_extResume"):'').'</p></td>
887 </tr>';
888 }
889
890 #debug(array($GLOBALS["TSFE"]->id,$GLOBALS["TSFE"]->sPre,$this->pi_getPageLink($GLOBALS["TSFE"]->id,$GLOBALS["TSFE"]->sPre)));
891
892 $out='<table '.$this->conf["tableParams."]["searchBox"].'>
893 <form action="'.$this->pi_getPageLink($GLOBALS["TSFE"]->id,$GLOBALS["TSFE"]->sPre).'" method="POST" name="'.$this->prefixId.'">
894 '.implode(chr(10),$rows).'
895 <input type="hidden" name="'.$this->prefixId.'[_sections]" value="0">
896 <input type="hidden" name="'.$this->prefixId.'[pointer]" value="0">
897 <input type="hidden" name="'.$this->prefixId.'[ext]" value="'.($this->piVars["ext"]?1:0).'">
898 </form>
899 </table>';
900 $out.='<p>'.
901 ($this->piVars["ext"] ?
902 '<a href="'.$this->pi_getPageLink($GLOBALS["TSFE"]->id,$GLOBALS["TSFE"]->sPre,array($this->prefixId."[ext]"=>0)).'">'.$this->pi_getLL("link_regularSearch").'</a>' :
903 '<a href="'.$this->pi_getPageLink($GLOBALS["TSFE"]->id,$GLOBALS["TSFE"]->sPre,array($this->prefixId."[ext]"=>1)).'">'.$this->pi_getLL("link_advancedSearch").'</a>'
904 ).'</p>';
905
906 return '<div'.$this->pi_classParam("searchbox").'>'.$out.'</div>';
907 }
908
909 /**
910 * Print the searching rules
911 */
912 function printRules() {
913 $out = "";
914 if ($this->conf["show."]["rules"]) {
915 $out = '<h2>'.$this->pi_getLL("rules_header").'</h2><p>'.nl2br(htmlspecialchars(trim($this->pi_getLL("rules_text")))).'</p>';
916 $out = '<div'.$this->pi_classParam("rules").'>'.$this->cObj->stdWrap($out, $this->conf["rules_stdWrap."]).'</div>';
917 }
918 return $out;
919 }
920
921 /**
922 * Returns the anchor-links to the sections inside the displayed result rows.
923 */
924 function printResultSectionLinks() {
925 $lines=array();
926 reset($this->resultSections);
927 while(list($id,$dat)=each($this->resultSections)) {
928 $lines[]='<li><a href="'.$this->anchorPrefix.'#'.md5($id).'">'.(trim($dat[0])?htmlspecialchars(trim($dat[0])):$this->pi_getLL("unnamedSection")).' ('.$dat[1].' '.$this->pi_getLL("word_page".($dat[1]>1?"s":"")).')</a></li>';
929 }
930 $out = '<ul>'.implode(chr(10),$lines).'</ul>';
931 return '<div'.$this->pi_classParam("sectionlinks").'>'.$this->cObj->stdWrap($out, $this->conf["sectionlinks_stdWrap."]).'</div>';
932 }
933
934 /**
935 * Returns the links for the result browser bar (next/prev/1-2-3)
936 */
937 function makePointerSelector($count,$displayCount,$pointer) {
938 $lines=array();
939
940 // Previous pointer:
941 if ($pointer>0) {
942 $lines[]=$this->makePointerSelector_link("PREV",0);
943 }
944
945 // 1-2-3
946 for ($a=0;$a<t3lib_div::intInRange(ceil($count/$displayCount),1,10);$a++) {
947 # $str = ($a*$displayCount+1)."-".(($a+1)*$displayCount);
948 $str = $a+1;
949 $linkStr = $this->makePointerSelector_link($str,$a);
950 $lines[]= $pointer==$a ? '<strong>['.$linkStr.']</strong>' : $linkStr;
951 }
952
953 // Next pointer:
954 if ($pointer+1<ceil($count/$displayCount)) $lines[]=$this->makePointerSelector_link("NEXT",$pointer+1);
955
956 return implode(" - ",$lines);
957 }
958
959 /**
960 * Used to make the link for the result-browser.
961 * Notive now the links must resubmit the form after setting the new pointer-value in a hidden formfield.
962 */
963 function makePointerSelector_link($str,$p) {
964 return '<a href="#" onClick="document.'.$this->prefixId.'[\''.$this->prefixId.'[pointer]\'].value=\''.$p.'\';document.'.$this->prefixId.'.submit();return false;">'.$str.'</a>';
965 }
966
967 /**
968 * Returns a string that tells which search words are searched for.
969 */
970 function tellUsWhatIsSeachedFor($sWArr) {
971 reset($sWArr);
972 $searchingFor="";
973 $c=0;
974 while(list($k,$v)=each($sWArr)) {
975 if ($c) {
976 switch($v["oper"]) {
977 case "OR":
978 $searchingFor.=" ".$this->pi_getLL("searchFor_or")." ".$this->wrapSW($v["sword"]);
979 break;
980 case "AND NOT":
981 $searchingFor.=" ".$this->pi_getLL("searchFor_butNot")." ".$this->wrapSW($v["sword"]);
982 break;
983 default: // AND...
984 $searchingFor.=" ".$this->pi_getLL("searchFor_and")." ".$this->wrapSW($v["sword"]);
985 break;
986 }
987
988 } else {
989 $searchingFor=$this->pi_getLL("searchFor")." ".$this->utf8_to_currentCharset($this->wrapSW($v["sword"]));
990 }
991 $c++;
992 }
993 return $searchingFor;
994 }
995
996 /**
997 * Wraps the search words in the search-word list display (from ->tellUsWhatIsSeachedFor())
998 */
999 function wrapSW($str) {
1000 return "'<span".$this->pi_classParam("sw").">".htmlspecialchars($str)."</span>'";
1001 }
1002
1003 /**
1004 * Makes a selector box
1005 */
1006 function renderSelectBox($name,$value,$optValues) {
1007 if (is_array($optValues)) {
1008 $opt=array();
1009 $isSelFlag=0;
1010 reset($optValues);
1011 while(list($k,$v)=each($optValues)) {
1012 $sel = (!strcmp($k,$value)?" SELECTED":"");
1013 if ($sel) $isSelFlag++;
1014 $opt[]='<option value="'.htmlspecialchars($k).'"'.$sel.'>'.htmlspecialchars($v).'</option>';
1015 }
1016 # if (!$isSelFlag && strcmp("",$value)) $opt[]='<option value="'.$value.'" SELECTED>'.htmlspecialchars("CURRENT VALUE '".$value."' DID NOT EXIST AMONG THE OPTIONS").'</option>';
1017 return '<select name="'.$name.'">'.implode("",$opt).'</select>';
1018 }
1019 }
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035 /***********************************
1036
1037 Result row LAYOUT
1038
1039 ***********************************/
1040
1041 /**
1042 * Takes the array with resultrows as input and returns the result-HTML-code
1043 * Takes the "group" var into account: Makes a "section" or "flat" display.
1044 */
1045 function compileResult($resultRows) {
1046 $content="";
1047
1048 $newResultRows=array();
1049 reset($resultRows);
1050 while(list(,$row)=each($resultRows)) {
1051 $id = md5($row["phash_grouping"]);
1052 if (is_array($newResultRows[$id])) {
1053 if (!$newResultRows[$id]["show_resume"] && $row["show_resume"]) { // swapping:
1054 // Remove old
1055 $subrows = $newResultRows[$id]["_sub"];
1056 unset($newResultRows[$id]["_sub"]);
1057 $subrows[] =$newResultRows[$id];
1058
1059 // Insert new:
1060 $newResultRows[$id]=$row;
1061 $newResultRows[$id]["_sub"]=$subrows;
1062 } else $newResultRows[$id]["_sub"][]=$row;
1063 } else {
1064 $newResultRows[$id]=$row;
1065 }
1066 }
1067 $resultRows=$newResultRows;
1068
1069
1070 switch($this->piVars["group"]) {
1071 case "sections":
1072 $rl2flag = substr($this->piVars["sections"],0,2)=="rl";
1073 $sections=array();
1074 reset($resultRows);
1075 while(list(,$row)=each($resultRows)) {
1076 $id = $row["rl0"]."-".$row["rl1"].($rl2flag?"-".$row["rl2"]:"");
1077 $sections[$id][]=$row;
1078 }
1079
1080 $this->resultSections=array();
1081 reset($sections);
1082 while(list($id,$resultRows)=each($sections)) {
1083 $rlParts = explode("-",$id);
1084
1085 $theId = $rlParts[2]?$rlParts[2]:($rlParts[1]?$rlParts[1]:$rlParts[0]);
1086 $theRLid = $rlParts[2]?"rl2_".$rlParts[2]:($rlParts[1]?"rl1_".$rlParts[1]:"0");
1087
1088 $sectionName = substr($this->getPathFromPageId($theId),1);
1089 if (!trim($sectionName)) {
1090 $sectionTitleLinked=$this->pi_getLL("unnamedSection").":";
1091 } else {
1092 $sectionTitleLinked = '<a href="#" onClick="document.'.$this->prefixId.'[\''.$this->prefixId.'[_sections]\'].value=\''.$theRLid.'\';document.'.$this->prefixId.'.submit();return false;">'.$sectionName.':</a>';
1093 }
1094
1095 $content.=$this->makeSectionHeader($id,$sectionTitleLinked,count($resultRows));
1096 $this->resultSections[$id] = array($sectionName,count($resultRows));
1097 reset($resultRows);
1098 while(list(,$row)=each($resultRows)) {
1099 $content.=$this->printResultRow($row);
1100 }
1101 }
1102 break;
1103 default: // flat:
1104 reset($resultRows);
1105 while(list(,$row)=each($resultRows)) {
1106 $content.=$this->printResultRow($row);
1107 }
1108 break;
1109 }
1110 return '<div'.$this->pi_classParam("res").'>'.$content.'</div>';
1111 }
1112
1113 /**
1114 * Returns the section header of the search result.
1115 */
1116 function makeSectionHeader($id,$sectionTitleLinked,$countResultRows) {
1117 return '<div'.$this->pi_classParam("secHead").'><a name="'.md5($id).'"></a><table '.$this->conf["tableParams."]["secHead"].'>
1118 <tr>
1119 <td width="95%"><h2>'.$sectionTitleLinked.'</h2></td>
1120 <td align="right" nowrap><p>'.$countResultRows.' '.$this->pi_getLL("word_page".($countResultRows>1?"s":"")).'</p></td>
1121 </tr>
1122 </table></div>';
1123 }
1124
1125 /**
1126 * This prints a single result row, including a recursive call for subrows.
1127 */
1128 function printResultRow($row,$headerOnly=0) {
1129 $specRowConf = $this->getSpecialConfigForRow($row);
1130 $CSSsuffix = $specRowConf["CSSsuffix"]?"-".$specRowConf["CSSsuffix"]:"";
1131
1132 // If external media, link to the media-file instead.
1133 if ($row["item_type"]) {
1134 if ($row["show_resume"]) { // Can link directly.
1135 $title = '<a href="'.$row["data_filename"].'">'.$row["result_number"].": ".$this->makeTitle($row).'</a>';
1136 } else { // Suspicious, so linking to page instead...
1137 $copy_row=$row;
1138 unset($copy_row["cHashParams"]);
1139 $title = $this->linkPage($row["page_id"],$row["result_number"].": ".$this->makeTitle($row),$copy_row);
1140 }
1141 } else { // Else the page:
1142 $title = $this->linkPage($row["data_page_id"],$row["result_number"].": ".$this->makeTitle($row),$row);
1143 }
1144
1145 // Make the header row with title, icon and rating bar.:
1146 $out.='<tr '.$this->pi_classParam("title".$CSSsuffix).'>
1147 <td width="16">'.$this->makeItemTypeIcon($row["item_type"],"",$specRowConf).'</td>
1148 <td width="95%" nowrap><p>'.$title.'</p></td>
1149 <td nowrap><p'.$this->pi_classParam("percent".$CSSsuffix).'>'.$this->makeRating($row).'</p></td>
1150 </tr>';
1151
1152 // Print the resume-section. If headerOnly is 1, then only the short resume is printed
1153 if (!$headerOnly) {
1154 $out.='<tr>
1155 <td></td>
1156 <td colspan=2'.$this->pi_classParam("descr".$CSSsuffix).'><p>'.$this->makeDescription($row,$this->piVars["extResume"]?0:1).'</p></td>
1157 </tr>';
1158 $out.='<tr>
1159 <td></td>
1160 <td '.$this->pi_classParam("info".$CSSsuffix).' nowrap><p>'.$this->makeInfo($row).'</p></td>
1161 <td '.$this->pi_classParam("info".$CSSsuffix).' align="right"><p>'.$this->makeAccessIndication($row["page_id"]).''.$this->makeLanguageIndication($row).'</p></td>
1162 </tr>';
1163 } elseif ($headerOnly==1) {
1164 $out.='<tr>
1165 <td></td>
1166 <td colspan=2'.$this->pi_classParam("descr".$CSSsuffix).'><p>'.$this->makeDescription($row,1,180).'</p></td>
1167 </tr>';
1168 } elseif ($headerOnly==2) {
1169 // nothing.
1170 }
1171
1172 // If there are subrows (eg. subpages in a PDF-file or if a duplicate page is selected due to user-login (phash_grouping))
1173 if (is_array($row["_sub"])) {
1174 if ($row["item_type"]==2) {
1175 $out.='<tr>
1176 <td></td>
1177 <td colspan=2><p><BR>'.$this->pi_getLL("res_otherMatching").'<BR><BR></p></td>
1178 </tr>';
1179
1180 reset($row["_sub"]);
1181 while(list(,$subRow)=each($row["_sub"])) {
1182 $out.='<tr>
1183 <td></td>
1184 <td colspan=2><p>'.$this->printResultRow($subRow,1).'</p></td>
1185 </tr>';
1186 }
1187 } else {
1188 $out.='<tr>
1189 <td></td>
1190 <td colspan=2><p>'.$this->pi_getLL("res_otherPageAsWell").'</p></td>
1191 </tr>';
1192 }
1193 }
1194
1195 return '<table '.$this->conf["tableParams."]["searchRes"].'>'.$out.'</table><BR>';
1196 }
1197
1198 /**
1199 * Return the icon corresponding to media type $it;
1200 */
1201 function makeItemTypeIcon($it,$alt="",$specRowConf=array()) {
1202 $spec_flag = 0;
1203
1204 switch($it) {
1205 case 1:
1206 $icon="html.gif";
1207 break;
1208 case 2:
1209 $icon="pdf.gif";
1210 break;
1211 case 3:
1212 $icon="doc.gif";
1213 break;
1214 case 4:
1215 $icon="txt.gif";
1216 break;
1217 default:
1218 $icon="pages.gif";
1219 if (is_array($specRowConf["pageIcon."])) {
1220 $spec_flag = 1;
1221 $spec = $this->cObj->IMAGE($specRowConf["pageIcon."]);
1222 }
1223 break;
1224 }
1225 if ($spec_flag) {
1226 return $spec;
1227 } else {
1228 $fullPath = t3lib_extMgm::siteRelPath("indexed_search").'pi/res/'.$icon;
1229 $info = @getimagesize(PATH_site.$fullPath);
1230
1231 return is_array($info) ? '<img src="'.$fullPath.'" hspace=3 '.$info[3].' title="'.htmlspecialchars($alt).'">' : '';
1232 }
1233 }
1234
1235 /**
1236 * Return the rating-HTML code for the result row. This makes use of the $this->firstRow
1237 */
1238 function makeRating($row) {
1239 /*
1240 "rank_flag" => "Rang, prioritet",
1241 "rank_freq" => "Rang, frekvens",
1242 "rank_first" => "Rang, tæt på toppen",
1243 "rank_count" => "Rang, antal forekomster",
1244 "rating" => "Side-rating",
1245 "hits" => "Side-hits",
1246 "title" => "Dokumenttitel",
1247 "crdate" => "Oprettelsesdato",
1248 "mtime" => "Ændringsdato",
1249 */
1250 switch((string)$this->piVars["order"]) {
1251 case "rank_count":
1252 return $row["order_val"]." matches";
1253 break;
1254 case "rank_first":
1255 return ceil(t3lib_div::intInRange(255-$row["order_val"],1,255)/255*100)."%";
1256 break;
1257 case "rank_flag":
1258 if ($this->firstRow["order_val2"]) {
1259 $base=$row["order_val1"]*256; // (3 MSB bit, 224 is highest value of order_val1 currently)
1260 $freqNumber = $row["order_val2"]/$this->firstRow["order_val2"]*pow(2,13); // 16-3 MSB = 13
1261 $total = t3lib_div::intInRange($base+$freqNumber,0,65536);
1262 #debug($total);
1263 #debug(log($total)/log(65536)*100,1);
1264 return ceil(log($total)/log(65536)*100)."%";
1265 }
1266 break;
1267 case "rank_freq":
1268 $max = 10000;
1269 $total = t3lib_div::intInRange($row["order_val"],0,$max);
1270 # debug($total);
1271 return ceil(log($total)/log($max)*100)."%";
1272 break;
1273 case "crdate":
1274 return $this->cObj->calcAge(time()-$row["item_crdate"],0); // ,$conf["age"]
1275 break;
1276 case "mtime":
1277 return $this->cObj->calcAge(time()-$row["item_mtime"],0); // ,$conf["age"]
1278 break;
1279 default: // fx. title
1280 # return "[rating...]";
1281 return "&nbsp;";
1282 break;
1283 }
1284 }
1285
1286 /**
1287 * Returns the resume for the search-result.
1288 * If noMarkup is NOT set, 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.
1289 */
1290 function makeDescription($row,$noMarkup=0,$lgd=180) {
1291 if ($row["show_resume"]) {
1292 #debug($row) ;
1293 if (!$noMarkup) {
1294 $markedSW = "";
1295 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'index_fulltext', 'phash='.intval($row['phash']));
1296 if ($ftdrow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
1297 $markedSW = $this->markupSWpartsOfString($ftdrow["fulltextdata"]);
1298 }
1299 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1300 }
1301
1302 return $this->utf8_to_currentCharset(htmlspecialchars(t3lib_div::fixed_lgd(str_replace("&nbsp;"," ",$row["item_description"]),$lgd)).(trim($markedSW)?" ... ".$markedSW:"")).'
1303 <BR><img src=clear.gif width=1 height=5>';
1304 } else {
1305 return '<font color="#666666">'.$this->pi_getLL("res_noResume").'
1306 <BR><img src=clear.gif width=1 height=5></font>';
1307 }
1308 }
1309
1310 /**
1311 * Marks up the search words from $this->sWarr in the $str with a color.
1312 */
1313 function markupSWpartsOfString($str) {
1314 $str = str_replace("&nbsp;"," ",t3lib_parsehtml::bidir_htmlspecialchars($str,-1));
1315 reset($this->sWArr);
1316 #debug($this->sWArr);
1317 $swForReg=array();
1318 while(list(,$d)=each($this->sWArr)) {
1319 $swForReg[]=quotemeta($d["sword"]);
1320 }
1321 $regExString = implode("|",$swForReg);
1322
1323 $noAlph="[^[:alnum:]]";
1324 $theType = (string)$this->piVars["type"];
1325 switch($theType) {
1326 case "1":
1327 case "20":
1328 // Nothing...
1329 break;
1330 case "2":
1331 $regExString = $noAlph."(".$regExString.")";
1332 break;
1333 case "3":
1334 $regExString = "(".$regExString.")".$noAlph;
1335 break;
1336 case "10":
1337 break;
1338 default:
1339 $regExString = $noAlph."(".$regExString.")".$noAlph;
1340 break;
1341 }
1342 #debug(array($regExString));
1343
1344 $parts = spliti($regExString,$str,6);
1345 #debug($parts);
1346 reset($parts);
1347 $strLen=0;
1348
1349 $snippets=array();
1350 while(list($k,$strP)=each($parts)) {
1351 if ($k+1<count($parts)) {
1352 $strLen+=strlen($strP);
1353 $reg=array();
1354 eregi("^".$regExString,substr($str,$strLen,50),$reg);
1355
1356 $snippets[] = array(
1357 # substr($str,$strLen-50,50),
1358 substr($parts[$k],-50),
1359 substr($str,$strLen,strlen($reg[0])),
1360 substr($parts[$k+1],0,50)
1361 # substr($str,$strLen+strlen($reg[0]),50),
1362 );
1363
1364 $strLen+=strlen($reg[0]);
1365 # debug($reg);
1366 }
1367 }
1368
1369 reset($snippets);
1370 $content=array();
1371 while(list(,$d)=each($snippets)) {
1372 $content[]=htmlspecialchars($d[0]).'<span'.$this->pi_classParam("redMarkup").'>'.htmlspecialchars($d[1]).'</span>'.htmlspecialchars($d[2]);
1373 }
1374
1375 return implode(" ... ",$content);
1376 }
1377
1378 /**
1379 * Returns the title of the search result row
1380 */
1381 function makeTitle($row) {
1382 $add="";
1383 if ($row["item_type"]==2) {
1384 $dat = unserialize($row["cHashParams"]);
1385 $pp=explode("-",$dat["key"]);
1386 if ($pp[0]!=$pp[1]) {
1387 $add=", ".$this->pi_getLL("word_pages")." ".$dat["key"];
1388 } else $add=", ".$this->pi_getLL("word_page")." ".$pp[0];
1389 }
1390 return $this->utf8_to_currentCharset(htmlspecialchars(t3lib_div::fixed_lgd($row["item_title"],50))).$add;
1391 }
1392
1393 /**
1394 * Returns the info-string in the bottom of the result-row display (size, dates, path)
1395 */
1396 function makeInfo($row) {
1397 $lines=array();
1398 $lines[]=$this->pi_getLL("res_size")." ".t3lib_div::formatSize($row["item_size"])."";
1399 $lines[]=$this->pi_getLL("res_created")." ".date("d-m-y",$row["item_crdate"])."";
1400 $lines[]=$this->pi_getLL("res_modified")." ".date("d-m-y H:i",$row["item_mtime"])."";
1401 $out = implode(" - ",$lines);
1402 $pathId = $row["data_page_id"]?$row["data_page_id"]:$row["page_id"];
1403 $pathMP = $row["data_page_id"]?$row["data_page_mp"]:"";
1404 $out.="<BR>".$this->pi_getLL("res_path")." ".$this->linkPage($pathId,htmlspecialchars($this->getPathFromPageId($pathId,$pathMP)));
1405 return $out;
1406 }
1407
1408 /**
1409 * Returns the info-string in the bottom of the result-row display (size, dates, path)
1410 */
1411 function getSpecialConfigForRow($row) {
1412 $pathId = $row["data_page_id"]?$row["data_page_id"]:$row["page_id"];
1413 $pathMP = $row["data_page_id"]?$row["data_page_mp"]:"";
1414
1415 $rl = $this->getRootLine($pathId,$pathMP);
1416 $specConf = $this->conf["specConfs."]["0."];
1417 if (is_array($rl)) {
1418 reset($rl);
1419 while(list(,$dat)=each($rl)) {
1420 if (is_array($this->conf["specConfs."][$dat["uid"]."."])) {
1421 $specConf = $this->conf["specConfs."][$dat["uid"]."."];
1422 break;
1423 }
1424 }
1425 }
1426
1427 return $specConf;
1428 }
1429
1430 /**
1431 * Returns the HTML code for language indication.
1432 */
1433 function makeLanguageIndication($row) {
1434 if ($row["item_type"]==0) {
1435 switch($row["sys_language_uid"]) {
1436 // OBVIOUSLY this will not work generally. First we need to know WHICH flag is used for WHICH language-uid. This just shows the concept works.
1437 case 1:
1438 return '<img src="tslib/media/flags/flag_dk.gif" width="21" height="13" border="0" alt="Danish">';
1439 break;
1440 case 2:
1441 return '<img src="tslib/media/flags/flag_de.gif" width="21" height="13" border="0" alt="German">';
1442 break;
1443 default:
1444 # return '<img src="tslib/media/flags/flag_uk.gif" width="21" height="13" border="0" alt="English - default">';
1445 break;
1446 }
1447 }
1448 return "&nbsp;";
1449 }
1450
1451 /**
1452 * Returns the HTML code for the locking symbol.
1453 */
1454 function makeAccessIndication($id) {
1455 if (is_array($this->fe_groups_required[$id]) && count($this->fe_groups_required[$id])) {
1456 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"),implode(",",array_unique($this->fe_groups_required[$id]))).'">';
1457 }
1458 }
1459
1460 /**
1461 * Links the $str to page $id
1462 */
1463 function linkPage($id,$str,$row=array()) {
1464 # return $str;
1465 $urlParameters = unserialize($row["cHashParams"]);
1466 # $urlParameters["cHash"] = t3lib_div::shortMD5(serialize($row["cHashParams"])); // not needed - it's there already...
1467 #debug($urlParameters);
1468
1469
1470 // 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...
1471 if (!is_array($this->domain_records[$id])) {
1472 $this->getPathFromPageId($id);
1473 }
1474
1475 // If external domain, then link to that:
1476 if (count($this->domain_records[$id])) {
1477 reset($this->domain_records[$id]);
1478 $firstDom = current($this->domain_records[$id]);
1479 $scheme = t3lib_div::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://';
1480
1481 $addParams="";
1482 if (is_array($urlParameters)) {
1483 if (count($urlParameters)) {
1484 reset($urlParameters);
1485 while(list($k,$v)=each($urlParameters)) {
1486 $addParams.="&".$k."=".rawurlencode($v);
1487 }
1488 }
1489 }
1490
1491 return '<a href="'.$scheme.$firstDom.'/index.php?id='.$id.$addParams.'" target="'.$this->conf["search."]["detect_sys_domain_records."]["target"].'">'.htmlspecialchars($str).'</a>';
1492 } else {
1493 return $this->pi_linkToPage($str,$id,$this->conf["result_link_target"],$urlParameters);
1494 }
1495 }
1496
1497 /**
1498 * Returns the path to the page $id
1499 */
1500 function getRootLine($id,$pathMP="") {
1501 $identStr = $id."|".$pathMP;
1502
1503 if (!isset($this->cache_path[$identStr])) {
1504 $this->cache_rl[$identStr] = $GLOBALS["TSFE"]->sys_page->getRootLine($id,$pathMP);
1505 }
1506 return $this->cache_rl[$identStr];
1507 }
1508
1509 /**
1510 * Gets the first sys_domain record for the page, $id
1511 */
1512 function getFirstSysDomainRecordForPage($id) {
1513 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('domainName', 'sys_domain', 'pid='.intval($id).$this->cObj->enableFields('sys_domain'), '', 'sorting');
1514 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
1515 return ereg_replace("\/$","",$row["domainName"]);
1516 }
1517
1518 /**
1519 * Returns the path to the page $id
1520 */
1521 function getPathFromPageId($id,$pathMP="") {
1522 // Here I imagine some caching...
1523 $identStr = $id."|".$pathMP;
1524
1525 if (!isset($this->cache_path[$identStr])) {
1526 $this->fe_groups_required[$id]=array();
1527 $this->domain_records[$id]=array();
1528 $rl = $this->getRootLine($id,$pathMP);
1529 $hitRoot=0;
1530 $path="";
1531 if (is_array($rl) && count($rl)) {
1532 reset($rl);
1533 while(list($k,$v)=each($rl)) {
1534 // Check fe_user
1535 if ($v["fe_group"] && ($v["uid"]==$id || $v["extendToSubpages"])) {
1536 $this->fe_groups_required[$id][]=$v["fe_group"];
1537 }
1538 // Check sys_domain.
1539 if ($this->conf["search."]["detect_sys_domain_records"]) {
1540 $sysDName = $this->getFirstSysDomainRecordForPage($v["uid"]);
1541 if ($sysDName) {
1542 $this->domain_records[$id][]=$sysDName;
1543 # debug($sysDName);
1544 // Set path accordingly:
1545 $path=$sysDName.$path;
1546 break;
1547 }
1548 }
1549
1550 // Stop, if we find that the current id is the current root page.
1551 if ($v["uid"]==$GLOBALS["TSFE"]->config["rootLine"][0]["uid"]) {
1552 break;
1553 }
1554 $path="/".$v["title"].$path;
1555 }
1556 }
1557
1558 # $pageRec = $GLOBALS["TSFE"]->sys_page->checkRecord("pages",$id);
1559 # $path.="/".$pageRec["title"];
1560 $this->cache_path[$identStr] = $path;
1561
1562 if (is_array($this->conf["path_stdWrap."])) {
1563 $this->cache_path[$identStr] = $this->cObj->stdWrap($this->cache_path[$identStr], $this->conf["path_stdWrap."]);
1564 }
1565 }
1566
1567 return $this->cache_path[$identStr];
1568 }
1569
1570 /**
1571 * Returns a results browser
1572 */
1573 function pi_list_browseresults($showResultCount=1,$addString="",$addPart="") {
1574
1575 // Initializing variables:
1576 $pointer=$this->piVars["pointer"];
1577 $count=$this->internal["res_count"];
1578 $results_at_a_time = t3lib_div::intInRange($this->internal["results_at_a_time"],1,1000);
1579 $maxPages = t3lib_div::intInRange($this->internal["maxPages"],1,100);
1580 $max = t3lib_div::intInRange(ceil($count/$results_at_a_time),1,$maxPages);
1581 $pointer=intval($pointer);
1582 $links=array();
1583
1584 // Make browse-table/links:
1585 if ($pointer>0) {
1586 $links[]='<td><p>'.$this->makePointerSelector_link($this->pi_getLL("pi_list_browseresults_prev","< Previous"),$pointer-1).'</p></td>';
1587 }
1588 for($a=0;$a<$max;$a++) {
1589 $links[]='<td'.($pointer==$a?$this->pi_classParam("browsebox-SCell"):"").'><p>'.$this->makePointerSelector_link(trim($this->pi_getLL("pi_list_browseresults_page","Page")." ".($a+1)),$a).'</p></td>';
1590 }
1591 if ($pointer<ceil($count/$results_at_a_time)-1) {
1592 $links[]='<td><p>'.$this->makePointerSelector_link($this->pi_getLL("pi_list_browseresults_next","Next >"),$pointer+1).'</p></td>';
1593 }
1594
1595 $pR1 = $pointer*$results_at_a_time+1;
1596 $pR2 = $pointer*$results_at_a_time+$results_at_a_time;
1597 $sTables = '<DIV'.$this->pi_classParam("browsebox").'>'.
1598 ($showResultCount ? '<P>'.sprintf(
1599 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>")),
1600 $pR1,
1601 min(array($this->internal["res_count"],$pR2)),
1602 $this->internal["res_count"]
1603 ).$addString.'</P>':''
1604 ).$addPart.
1605 '<table>
1606 <tr>'.implode("",$links).'</tr>
1607 </table></DIV>';
1608 return $sTables;
1609 }
1610
1611 /**
1612 * Return the menu of pages used for the selector.
1613 */
1614 function getMenu($id) {
1615 if ($this->conf["show."]["LxALLtypes"]) {
1616 $output = Array();
1617 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('title,uid', 'pages', 'pid='.intval($id).$this->cObj->enableFields('pages'), '', 'sorting');
1618 while($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
1619 $output[$row["uid"]]=$GLOBALS["TSFE"]->sys_page->getPageOverlay($row);
1620 }
1621 return $output;
1622 } else {
1623 return $GLOBALS["TSFE"]->sys_page->getMenu($id);
1624 }
1625 }
1626
1627
1628 /**
1629 * Converts the input string from utf-8 to the backend charset.
1630 *
1631 * @param string String to convert (utf-8)
1632 * @return string Converted string (backend charset if different from utf-8)
1633 */
1634 function utf8_to_currentCharset($str) {
1635 return $GLOBALS['TSFE']->csConv($str,'utf-8');
1636 }
1637
1638 }
1639
1640 if (defined("TYPO3_MODE") && $TYPO3_CONF_VARS[TYPO3_MODE]["XCLASS"]["ext/indexed_search/pi/class.tx_indexedsearch.php"]) {
1641 include_once($TYPO3_CONF_VARS[TYPO3_MODE]["XCLASS"]["ext/indexed_search/pi/class.tx_indexedsearch.php"]);
1642 }
1643
1644 ?>