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