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