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