[TASK] Use arrays in str_replace() calls
[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');
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'], TRUE));
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', '', TRUE) . '</p>';
695 }
696 $GLOBALS['TT']->pull();
697 } else {
698 $content .= '<p' . $this->pi_classParam('noresults') . '>' . $this->pi_getLL('noResults', '', TRUE) . '</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', '', TRUE) . ' "' . 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', '', TRUE) . ':';
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 $resultlist = '';
771 foreach ($resultRows as $row) {
772 $resultlist .= $this->printResultRow($row);
773 }
774 $content .= $this->cObj->stdWrap($resultlist, $this->conf['resultlist_stdWrap.']);
775 }
776 break;
777 default:
778 // flat:
779 $resultlist = '';
780 foreach ($resultRows as $row) {
781 $resultlist .= $this->printResultRow($row);
782 }
783 $content .= $this->cObj->stdWrap($resultlist, $this->conf['resultlist_stdWrap.']);
784 }
785 } else {
786 $resultlist = '';
787 foreach ($resultRows as $row) {
788 $resultlist .= $this->printResultRow($row);
789 }
790 $content .= $this->cObj->stdWrap($resultlist, $this->conf['resultlist_stdWrap.']);
791 }
792 return '<div' . $this->pi_classParam('res') . '>' . $content . '</div>';
793 }
794
795 /***********************************
796 *
797 * Searching functions (SQL)
798 *
799 ***********************************/
800 /**
801 * Returns a COMPLETE list of phash-integers matching the search-result composed of the search-words in the sWArr array.
802 * The list of phash integers are unsorted and should be used for subsequent selection of index_phash records for display of the result.
803 *
804 * @param array Search word array
805 * @return string List of integers
806 * @todo Define visibility
807 */
808 public function getPhashList($sWArr) {
809 // Initialize variables:
810 $c = 0;
811 $totalHashList = array();
812 // This array accumulates the phash-values
813 // Traverse searchwords; for each, select all phash integers and merge/diff/intersect them with previous word (based on operator)
814 foreach ($sWArr as $k => $v) {
815 // Making the query for a single search word based on the search-type
816 $sWord = $v['sword'];
817 $theType = (string) $this->piVars['type'];
818 if (strstr($sWord, ' ')) {
819 // If there are spaces in the search-word, make a full text search instead.
820 $theType = 20;
821 }
822 $GLOBALS['TT']->push('SearchWord "' . $sWord . '" - $theType=' . $theType);
823 // Perform search for word:
824 switch ($theType) {
825 case '1':
826 // Part of word
827 $res = $this->searchWord($sWord, self::WILDCARD_LEFT | self::WILDCARD_RIGHT);
828 break;
829 case '2':
830 // First part of word
831 $res = $this->searchWord($sWord, self::WILDCARD_RIGHT);
832 break;
833 case '3':
834 // Last part of word
835 $res = $this->searchWord($sWord, self::WILDCARD_LEFT);
836 break;
837 case '10':
838 // Sounds like
839 /**
840 * Indexer object
841 *
842 * @var \TYPO3\CMS\IndexedSearch\Indexer
843 */
844 // Initialize the indexer-class
845 $indexerObj = GeneralUtility::makeInstance('TYPO3\\CMS\\IndexedSearch\\Indexer');
846 // Perform metaphone search
847 $res = $this->searchMetaphone($indexerObj->metaphone($sWord, $this->storeMetaphoneInfoAsWords));
848 unset($indexerObj);
849 break;
850 case '20':
851 // Sentence
852 $res = $this->searchSentence($sWord);
853 $this->piVars['order'] = 'mtime';
854 // 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.
855 break;
856 default:
857 // Distinct word
858 $res = $this->searchDistinct($sWord);
859 }
860 // If there was a query to do, then select all phash-integers which resulted from this.
861 if ($res) {
862 // Get phash list by searching for it:
863 $phashList = array();
864 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
865 $phashList[] = $row['phash'];
866 }
867 $GLOBALS['TYPO3_DB']->sql_free_result($res);
868 // Here the phash list are merged with the existing result based on whether we are dealing with OR, NOT or AND operations.
869 if ($c) {
870 switch ($v['oper']) {
871 case 'OR':
872 $totalHashList = array_unique(array_merge($phashList, $totalHashList));
873 break;
874 case 'AND NOT':
875 $totalHashList = array_diff($totalHashList, $phashList);
876 break;
877 default:
878 // AND...
879 $totalHashList = array_intersect($totalHashList, $phashList);
880 }
881 } else {
882 $totalHashList = $phashList;
883 }
884 }
885 $GLOBALS['TT']->pull();
886 $c++;
887 }
888 return implode(',', $totalHashList);
889 }
890
891 /**
892 * Returns a query which selects the search-word from the word/rel tables.
893 *
894 * @param string WHERE clause selecting the word from phash
895 * @param string Additional AND clause in the end of the query.
896 * @return pointer SQL result pointer
897 * @todo Define visibility
898 */
899 public function execPHashListQuery($wordSel, $plusQ = '') {
900 return $GLOBALS['TYPO3_DB']->exec_SELECTquery('IR.phash', 'index_words IW,
901 index_rel IR,
902 index_section ISEC', $wordSel . '
903 AND IW.wid=IR.wid
904 AND ISEC.phash = IR.phash
905 ' . $this->sectionTableWhere() . '
906 ' . $plusQ, 'IR.phash');
907 }
908
909 /**
910 * Search for a word
911 *
912 * @param string $sWord Word to search for
913 * @param integer $mode Bit-field which can contain WILDCARD_LEFT and/or WILDCARD_RIGHT
914 * @return pointer SQL result pointer
915 * @todo Define visibility
916 */
917 public function searchWord($sWord, $mode) {
918 $wildcard_left = $mode & self::WILDCARD_LEFT ? '%' : '';
919 $wildcard_right = $mode & self::WILDCARD_RIGHT ? '%' : '';
920 $wSel = 'IW.baseword LIKE \'' . $wildcard_left . $GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_words') . $wildcard_right . '\'';
921 $res = $this->execPHashListQuery($wSel, ' AND is_stopword=0');
922 return $res;
923 }
924
925 /**
926 * Search for one distinct word
927 *
928 * @param string $sWord Word to search for
929 * @return pointer SQL result pointer
930 * @todo Define visibility
931 */
932 public function searchDistinct($sWord) {
933 $wSel = 'IW.wid=' . \TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::md5inthash($sWord);
934 $res = $this->execPHashListQuery($wSel, ' AND is_stopword=0');
935 return $res;
936 }
937
938 /**
939 * Search for a sentence
940 *
941 * @param string $sSentence Sentence to search for
942 * @return pointer SQL result pointer
943 * @todo Define visibility
944 */
945 public function searchSentence($sSentence) {
946 $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
947 ISEC.phash = IFT.phash
948 ' . $this->sectionTableWhere(), 'ISEC.phash');
949 return $res;
950 }
951
952 /**
953 * Search for a metaphone word
954 *
955 * @param string $sWord Word to search for
956 * @return pointer SQL result pointer
957 * @todo Define visibility
958 */
959 public function searchMetaphone($sWord) {
960 $wSel = 'IW.metaphone=' . $sWord;
961 $res = $this->execPHashListQuery($wSel, ' AND is_stopword=0');
962 }
963
964 /**
965 * Returns AND statement for selection of section in database. (rootlevel 0-2 + page_id)
966 *
967 * @return string AND clause for selection of section in database.
968 * @todo Define visibility
969 */
970 public function sectionTableWhere() {
971 $out = $this->wholeSiteIdList < 0 ? '' : ' AND ISEC.rl0 IN (' . $this->wholeSiteIdList . ')';
972 $match = '';
973 if (substr($this->piVars['sections'], 0, 4) == 'rl1_') {
974 $list = implode(',', GeneralUtility::intExplode(',', substr($this->piVars['sections'], 4)));
975 $out .= ' AND ISEC.rl1 IN (' . $list . ')';
976 $match = TRUE;
977 } elseif (substr($this->piVars['sections'], 0, 4) == 'rl2_') {
978 $list = implode(',', GeneralUtility::intExplode(',', substr($this->piVars['sections'], 4)));
979 $out .= ' AND ISEC.rl2 IN (' . $list . ')';
980 $match = TRUE;
981 } elseif (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'])) {
982 // Traversing user configured fields to see if any of those are used to limit search to a section:
983 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'] as $fieldName => $rootLineLevel) {
984 if (substr($this->piVars['sections'], 0, strlen($fieldName) + 1) == $fieldName . '_') {
985 $list = implode(',', GeneralUtility::intExplode(',', substr($this->piVars['sections'], strlen($fieldName) + 1)));
986 $out .= ' AND ISEC.' . $fieldName . ' IN (' . $list . ')';
987 $match = TRUE;
988 break;
989 }
990 }
991 }
992 // If no match above, test the static types:
993 if (!$match) {
994 switch ((string) $this->piVars['sections']) {
995 case '-1':
996 // '-1' => 'Only this page',
997 $out .= ' AND ISEC.page_id=' . $GLOBALS['TSFE']->id;
998 break;
999 case '-2':
1000 // '-2' => 'Top + level 1',
1001 $out .= ' AND ISEC.rl2=0';
1002 break;
1003 case '-3':
1004 // '-3' => 'Level 2 and out',
1005 $out .= ' AND ISEC.rl2>0';
1006 break;
1007 }
1008 }
1009 return $out;
1010 }
1011
1012 /**
1013 * Returns AND statement for selection of media type
1014 *
1015 * @return string AND statement for selection of media type
1016 * @todo Define visibility
1017 */
1018 public function mediaTypeWhere() {
1019 switch ((string) $this->piVars['media']) {
1020 case '0':
1021 // '0' => 'Kun TYPO3 sider',
1022 $out = ' AND IP.item_type=' . $GLOBALS['TYPO3_DB']->fullQuoteStr('0', 'index_phash');
1023 break;
1024 case '-2':
1025 // All external documents
1026 $out = ' AND IP.item_type<>' . $GLOBALS['TYPO3_DB']->fullQuoteStr('0', 'index_phash');
1027 break;
1028 case '-1':
1029 // All content
1030 $out = '';
1031 break;
1032 default:
1033 $out = ' AND IP.item_type=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->piVars['media'], 'index_phash');
1034 }
1035 return $out;
1036 }
1037
1038 /**
1039 * Returns AND statement for selection of langauge
1040 *
1041 * @return string AND statement for selection of langauge
1042 * @todo Define visibility
1043 */
1044 public function languageWhere() {
1045 if ($this->piVars['lang'] >= 0) {
1046 // -1 is the same as ALL language.
1047 return 'AND IP.sys_language_uid=' . intval($this->piVars['lang']);
1048 }
1049 }
1050
1051 /**
1052 * Where-clause for free index-uid value.
1053 *
1054 * @param integer Free Index UID value to limit search to.
1055 * @return string WHERE SQL clause part.
1056 * @todo Define visibility
1057 */
1058 public function freeIndexUidWhere($freeIndexUid) {
1059 if ($freeIndexUid >= 0) {
1060 // First, look if the freeIndexUid is a meta configuration:
1061 $indexCfgRec = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('indexcfgs', 'index_config', 'type=5 AND uid=' . intval($freeIndexUid) . $this->cObj->enableFields('index_config'));
1062 if (is_array($indexCfgRec)) {
1063 $refs = GeneralUtility::trimExplode(',', $indexCfgRec['indexcfgs']);
1064 $list = array(-99);
1065 // Default value to protect against empty array.
1066 foreach ($refs as $ref) {
1067 list($table, $uid) = GeneralUtility::revExplode('_', $ref, 2);
1068 switch ($table) {
1069 case 'index_config':
1070 $idxRec = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('uid', 'index_config', 'uid=' . intval($uid) . $this->cObj->enableFields('index_config'));
1071 if ($idxRec) {
1072 $list[] = $uid;
1073 }
1074 break;
1075 case 'pages':
1076 $indexCfgRecordsFromPid = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid', 'index_config', 'pid=' . intval($uid) . $this->cObj->enableFields('index_config'));
1077 foreach ($indexCfgRecordsFromPid as $idxRec) {
1078 $list[] = $idxRec['uid'];
1079 }
1080 break;
1081 }
1082 }
1083 $list = array_unique($list);
1084 } else {
1085 $list = array(intval($freeIndexUid));
1086 }
1087 return ' AND IP.freeIndexUid IN (' . implode(',', $list) . ')';
1088 }
1089 }
1090
1091 /**
1092 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
1093 *
1094 * @param string List of phash integers which match the search.
1095 * @param integer Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
1096 * @return pointer Query result pointer
1097 * @todo Define visibility
1098 */
1099 public function execFinalQuery($list, $freeIndexUid = -1) {
1100 // Setting up methods of filtering results based on page types, access, etc.
1101 $page_join = '';
1102 $page_where = '';
1103 // Indexing configuration clause:
1104 $freeIndexUidClause = $this->freeIndexUidWhere($freeIndexUid);
1105 // Calling hook for alternative creation of page ID list
1106 if ($hookObj = $this->hookRequest('execFinalQuery_idList')) {
1107 $page_where = $hookObj->execFinalQuery_idList($list);
1108 } elseif ($this->join_pages) {
1109 // Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
1110 $page_join = ',
1111 pages';
1112 $page_where = 'pages.uid = ISEC.page_id
1113 ' . $this->cObj->enableFields('pages') . '
1114 AND pages.no_search=0
1115 AND pages.doktype<200
1116 ';
1117 } elseif ($this->wholeSiteIdList >= 0) {
1118 // Collecting all pages IDs in which to search; filtering out ALL pages that are not accessible due to enableFields. Does NOT look for "no_search" field!
1119 $siteIdNumbers = GeneralUtility::intExplode(',', $this->wholeSiteIdList);
1120 $id_list = array();
1121 foreach ($siteIdNumbers as $rootId) {
1122 $id_list[] = $this->cObj->getTreeList(-1 * $rootId, 9999);
1123 }
1124 $page_where = ' ISEC.page_id IN (' . implode(',', $id_list) . ')';
1125 } else {
1126 // Disable everything... (select all)
1127 $page_where = ' 1=1 ';
1128 }
1129 // If any of the ranking sortings are selected, we must make a join with the word/rel-table again, because we need to calculate ranking based on all search-words found.
1130 if (substr($this->piVars['order'], 0, 5) == 'rank_') {
1131 switch ($this->piVars['order']) {
1132 case 'rank_flag':
1133 // This gives priority to word-position (max-value) so that words in title, keywords, description counts more than in content.
1134 // The ordering is refined with the frequency sum as well.
1135 $grsel = 'MAX(IR.flags) AS order_val1, SUM(IR.freq) AS order_val2';
1136 $orderBy = 'order_val1' . $this->isDescending() . ',order_val2' . $this->isDescending();
1137 break;
1138 case 'rank_first':
1139 // Results in average position of search words on page. Must be inversely sorted (low numbers are closer to top)
1140 $grsel = 'AVG(IR.first) AS order_val';
1141 $orderBy = 'order_val' . $this->isDescending(1);
1142 break;
1143 case 'rank_count':
1144 // Number of words found
1145 $grsel = 'SUM(IR.count) AS order_val';
1146 $orderBy = 'order_val' . $this->isDescending();
1147 break;
1148 default:
1149 // Frequency sum. I'm not sure if this is the best way to do it (make a sum...). Or should it be the average?
1150 $grsel = 'SUM(IR.freq) AS order_val';
1151 $orderBy = 'order_val' . $this->isDescending();
1152 }
1153 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('ISEC.*, IP.*, ' . $grsel, 'index_words IW,
1154 index_rel IR,
1155 index_section ISEC,
1156 index_phash IP' . $page_join, 'IP.phash IN (' . $list . ') ' . $this->mediaTypeWhere() . ' ' . $this->languageWhere() . $freeIndexUidClause . '
1157 AND IW.wid=IR.wid
1158 AND ISEC.phash = IR.phash
1159 AND IP.phash = IR.phash
1160 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);
1161 } else {
1162 // Otherwise, if sorting are done with the pages table or other fields, there is no need for joining with the rel/word tables:
1163 $orderBy = '';
1164 switch ((string) $this->piVars['order']) {
1165 case 'title':
1166 $orderBy = 'IP.item_title' . $this->isDescending();
1167 break;
1168 case 'crdate':
1169 $orderBy = 'IP.item_crdate' . $this->isDescending();
1170 break;
1171 case 'mtime':
1172 $orderBy = 'IP.item_mtime' . $this->isDescending();
1173 break;
1174 }
1175 $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 . '
1176 AND IP.phash = ISEC.phash
1177 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);
1178 }
1179 return $res;
1180 }
1181
1182 /**
1183 * Checking if the resume can be shown for the search result (depending on whether the rights are OK)
1184 * ? Should it also check for gr_list "0,-1"?
1185 *
1186 * @param array Result row array.
1187 * @return boolean Returns TRUE if resume can safely be shown
1188 * @todo Define visibility
1189 */
1190 public function checkResume($row) {
1191 // If the record is indexed by an indexing configuration, just show it.
1192 // At least this is needed for external URLs and files.
1193 // For records we might need to extend this - for instance block display if record is access restricted.
1194 if ($row['freeIndexUid']) {
1195 return TRUE;
1196 }
1197 // Evaluate regularly indexed pages based on item_type:
1198 if ($row['item_type']) {
1199 // External media:
1200 // For external media we will check the access of the parent page on which the media was linked from.
1201 // "phash_t3" is the phash of the parent TYPO3 page row which initiated the indexing of the documents in this section.
1202 // So, selecting for the grlist records belonging to the parent phash-row where the current users gr_list exists will help us to know.
1203 // 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.
1204 if (\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_grlist')) {
1205 $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'));
1206 } else {
1207 $res = FALSE;
1208 }
1209 if ($res && $GLOBALS['TYPO3_DB']->sql_num_rows($res)) {
1210 return TRUE;
1211 } else {
1212 return FALSE;
1213 }
1214 } else {
1215 // Ordinary TYPO3 pages:
1216 if (strcmp($row['gr_list'], $GLOBALS['TSFE']->gr_list)) {
1217 // 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...
1218 if (\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_grlist')) {
1219 $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'));
1220 } else {
1221 $res = FALSE;
1222 }
1223 if ($res && $GLOBALS['TYPO3_DB']->sql_num_rows($res)) {
1224 return TRUE;
1225 } else {
1226 return FALSE;
1227 }
1228 } else {
1229 return TRUE;
1230 }
1231 }
1232 }
1233
1234 /**
1235 * Check if the record is still available or if it has been deleted meanwhile.
1236 * Currently this works for files only, since extending it to page content would cause a lot of overhead.
1237 *
1238 * @param array Result row array
1239 * @return boolean Returns TRUE if record is still available
1240 * @todo Define visibility
1241 */
1242 public function checkExistance($row) {
1243 $recordExists = TRUE;
1244 // Always expect that page content exists
1245 if ($row['item_type']) {
1246 // External media:
1247 if (!is_file($row['data_filename']) || !file_exists($row['data_filename'])) {
1248 $recordExists = FALSE;
1249 }
1250 }
1251 return $recordExists;
1252 }
1253
1254 /**
1255 * Returns "DESC" or "" depending on the settings of the incoming highest/lowest result order (piVars['desc']
1256 *
1257 * @param boolean If TRUE, inverse the order which is defined by piVars['desc']
1258 * @return string " DESC" or
1259 * @todo Define visibility
1260 */
1261 public function isDescending($inverse = FALSE) {
1262 $desc = $this->piVars['desc'];
1263 if ($inverse) {
1264 $desc = !$desc;
1265 }
1266 return !$desc ? ' DESC' : '';
1267 }
1268
1269 /**
1270 * Write statistics information to database for the search operation
1271 *
1272 * @param array Search Word array
1273 * @param integer Number of hits
1274 * @param integer Milliseconds the search took
1275 * @return void
1276 * @todo Define visibility
1277 */
1278 public function writeSearchStat($sWArr, $count, $pt) {
1279 $insertFields = array(
1280 'searchstring' => $this->piVars['sword'],
1281 'searchoptions' => serialize(array($this->piVars, $sWArr, $pt)),
1282 'feuser_id' => intval($this->fe_user->user['uid']),
1283 // fe_user id, integer
1284 'cookie' => $this->fe_user->id,
1285 // cookie as set or retrieve. If people has cookies disabled this will vary all the time...
1286 'IP' => GeneralUtility::getIndpEnv('REMOTE_ADDR'),
1287 // Remote IP address
1288 'hits' => intval($count),
1289 // Number of hits on the search.
1290 'tstamp' => $GLOBALS['EXEC_TIME']
1291 );
1292 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_search', $insertFields);
1293 $newId = $GLOBALS['TYPO3_DB']->sql_insert_id();
1294 if ($newId) {
1295 foreach ($sWArr as $val) {
1296 $insertFields = array(
1297 'word' => $val['sword'],
1298 'index_stat_search_id' => $newId,
1299 'tstamp' => $GLOBALS['EXEC_TIME'],
1300 // Time stamp
1301 'pageid' => $GLOBALS['TSFE']->id
1302 );
1303 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_word', $insertFields);
1304 }
1305 }
1306 }
1307
1308 /***********************************
1309 *
1310 * HTML output functions
1311 *
1312 ***********************************/
1313 /**
1314 * Make search form HTML
1315 *
1316 * @param array Value/Labels pairs for search form selector boxes.
1317 * @return string Search form HTML
1318 * @todo Define visibility
1319 */
1320 public function makeSearchForm($optValues) {
1321 $html = $this->cObj->getSubpart($this->templateCode, '###SEARCH_FORM###');
1322 // Multilangual text
1323 $substituteArray = array('legend', 'searchFor', 'extResume', 'atATime', 'orderBy', 'fromSection', 'searchIn', 'match', 'style', 'freeIndexUid');
1324 foreach ($substituteArray as $marker) {
1325 $markerArray['###FORM_' . GeneralUtility::strtoupper($marker) . '###'] = $this->pi_getLL('form_' . $marker, '', TRUE);
1326 }
1327 $markerArray['###FORM_SUBMIT###'] = $this->pi_getLL('submit_button_label', '', TRUE);
1328 // Adding search field value
1329 $markerArray['###SWORD_VALUE###'] = htmlspecialchars($this->piVars['sword']);
1330 // Additonal keyword => "Add to current search words"
1331 if ($this->conf['show.']['clearSearchBox'] && $this->conf['show.']['clearSearchBox.']['enableSubSearchCheckBox']) {
1332 $markerArray['###SWORD_PREV_VALUE###'] = htmlspecialchars($this->conf['show.']['clearSearchBox'] ? '' : $this->piVars['sword']);
1333 $markerArray['###SWORD_PREV_INCLUDE_CHECKED###'] = $this->piVars['sword_prev_include'] ? ' checked="checked"' : '';
1334 $markerArray['###ADD_TO_CURRENT_SEARCH###'] = $this->pi_getLL('makerating_addToCurrentSearch', '', TRUE);
1335 } else {
1336 $html = $this->cObj->substituteSubpart($html, '###ADDITONAL_KEYWORD###', '');
1337 }
1338 $markerArray['###ACTION_URL###'] = htmlspecialchars($this->getSearchFormActionURL());
1339 $hiddenFieldCode = $this->cObj->getSubpart($this->templateCode, '###HIDDEN_FIELDS###');
1340 $hiddenFieldCode = preg_replace('/^\\n\\t(.+)/ms', '$1', $hiddenFieldCode);
1341 // Remove first newline and tab (cosmetical issue)
1342 $hiddenFieldArr = array();
1343 foreach (GeneralUtility::trimExplode(',', $this->hiddenFieldList) as $fieldName) {
1344 $hiddenFieldMarkerArray = array();
1345 $hiddenFieldMarkerArray['###HIDDEN_FIELDNAME###'] = $this->prefixId . '[' . $fieldName . ']';
1346 $hiddenFieldMarkerArray['###HIDDEN_VALUE###'] = htmlspecialchars((string) $this->piVars[$fieldName]);
1347 $hiddenFieldArr[$fieldName] = $this->cObj->substituteMarkerArrayCached($hiddenFieldCode, $hiddenFieldMarkerArray, array(), array());
1348 }
1349 // Extended search
1350 if ($this->piVars['ext']) {
1351 // Search for
1352 if (!is_array($optValues['type']) && !is_array($optValues['defOp']) || $this->conf['blind.']['type'] && $this->conf['blind.']['defOp']) {
1353 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_FOR###', '');
1354 } else {
1355 if (is_array($optValues['type']) && !$this->conf['blind.']['type']) {
1356 unset($hiddenFieldArr['type']);
1357 $markerArray['###SELECTBOX_TYPE_VALUES###'] = $this->renderSelectBoxValues($this->piVars['type'], $optValues['type']);
1358 } else {
1359 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_TYPE###', '');
1360 }
1361 if (is_array($optValues['defOp']) || !$this->conf['blind.']['defOp']) {
1362 $markerArray['###SELECTBOX_DEFOP_VALUES###'] = $this->renderSelectBoxValues($this->piVars['defOp'], $optValues['defOp']);
1363 } else {
1364 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_DEFOP###', '');
1365 }
1366 }
1367 // Search in
1368 if (!is_array($optValues['media']) && !is_array($optValues['lang']) || $this->conf['blind.']['media'] && $this->conf['blind.']['lang']) {
1369 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_IN###', '');
1370 } else {
1371 if (is_array($optValues['media']) && !$this->conf['blind.']['media']) {
1372 unset($hiddenFieldArr['media']);
1373 $markerArray['###SELECTBOX_MEDIA_VALUES###'] = $this->renderSelectBoxValues($this->piVars['media'], $optValues['media']);
1374 } else {
1375 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_MEDIA###', '');
1376 }
1377 if (is_array($optValues['lang']) || !$this->conf['blind.']['lang']) {
1378 unset($hiddenFieldArr['lang']);
1379 $markerArray['###SELECTBOX_LANG_VALUES###'] = $this->renderSelectBoxValues($this->piVars['lang'], $optValues['lang']);
1380 } else {
1381 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_LANG###', '');
1382 }
1383 }
1384 // Sections
1385 if (!is_array($optValues['sections']) || $this->conf['blind.']['sections']) {
1386 $html = $this->cObj->substituteSubpart($html, '###SELECT_SECTION###', '');
1387 } else {
1388 $markerArray['###SELECTBOX_SECTIONS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['sections'], $optValues['sections']);
1389 }
1390 // Free Indexing Configurations:
1391 if (!is_array($optValues['freeIndexUid']) || $this->conf['blind.']['freeIndexUid']) {
1392 $html = $this->cObj->substituteSubpart($html, '###SELECT_FREEINDEXUID###', '');
1393 } else {
1394 $markerArray['###SELECTBOX_FREEINDEXUIDS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['freeIndexUid'], $optValues['freeIndexUid']);
1395 }
1396 // Sorting
1397 if (!is_array($optValues['order']) || !is_array($optValues['desc']) || $this->conf['blind.']['order']) {
1398 $html = $this->cObj->substituteSubpart($html, '###SELECT_ORDER###', '');
1399 } else {
1400 unset($hiddenFieldArr['order']);
1401 unset($hiddenFieldArr['desc']);
1402 unset($hiddenFieldArr['results']);
1403 $markerArray['###SELECTBOX_ORDER_VALUES###'] = $this->renderSelectBoxValues($this->piVars['order'], $optValues['order']);
1404 $markerArray['###SELECTBOX_DESC_VALUES###'] = $this->renderSelectBoxValues($this->piVars['desc'], $optValues['desc']);
1405 $markerArray['###SELECTBOX_RESULTS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['results'], $optValues['results']);
1406 }
1407 // Limits
1408 if (!is_array($optValues['results']) || !is_array($optValues['results']) || $this->conf['blind.']['results']) {
1409 $html = $this->cObj->substituteSubpart($html, '###SELECT_RESULTS###', '');
1410 } else {
1411 $markerArray['###SELECTBOX_RESULTS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['results'], $optValues['results']);
1412 }
1413 // Grouping
1414 if (!is_array($optValues['group']) || $this->conf['blind.']['group']) {
1415 $html = $this->cObj->substituteSubpart($html, '###SELECT_GROUP###', '');
1416 } else {
1417 unset($hiddenFieldArr['group']);
1418 $markerArray['###SELECTBOX_GROUP_VALUES###'] = $this->renderSelectBoxValues($this->piVars['group'], $optValues['group']);
1419 }
1420 if ($this->conf['blind.']['extResume']) {
1421 $html = $this->cObj->substituteSubpart($html, '###SELECT_EXTRESUME###', '');
1422 } else {
1423 $markerArray['###EXT_RESUME_CHECKED###'] = $this->piVars['extResume'] ? ' checked="checked"' : '';
1424 }
1425 } else {
1426 // Extended search
1427 $html = $this->cObj->substituteSubpart($html, '###SEARCH_FORM_EXTENDED###', '');
1428 }
1429 if ($this->conf['show.']['advancedSearchLink']) {
1430 $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));
1431 $markerArray['###LINKTOOTHERMODE###'] = '<a href="' . htmlspecialchars($linkToOtherMode) . '">' . $this->pi_getLL(($this->piVars['ext'] ? 'link_regularSearch' : 'link_advancedSearch'), '', TRUE) . '</a>';
1432 } else {
1433 $markerArray['###LINKTOOTHERMODE###'] = '';
1434 }
1435 // Write all hidden fields
1436 $html = $this->cObj->substituteSubpart($html, '###HIDDEN_FIELDS###', implode('', $hiddenFieldArr));
1437 $substitutedContent = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1438 return $substitutedContent;
1439 }
1440
1441 /**
1442 * Function, rendering selector box values.
1443 *
1444 * @param string Current value
1445 * @param array Array with the options as key=>value pairs
1446 * @return string <options> imploded.
1447 * @todo Define visibility
1448 */
1449 public function renderSelectBoxValues($value, $optValues) {
1450 if (is_array($optValues)) {
1451 $opt = array();
1452 $isSelFlag = 0;
1453 foreach ($optValues as $k => $v) {
1454 $sel = !strcmp($k, $value) ? ' selected="selected"' : '';
1455 if ($sel) {
1456 $isSelFlag++;
1457 }
1458 $opt[] = '<option value="' . htmlspecialchars($k) . '"' . $sel . '>' . htmlspecialchars($v) . '</option>';
1459 }
1460 return implode('', $opt);
1461 }
1462 }
1463
1464 /**
1465 * Print the searching rules
1466 *
1467 * @return string Rules for the search
1468 * @todo Define visibility
1469 */
1470 public function printRules() {
1471 if ($this->conf['show.']['rules']) {
1472 $html = $this->cObj->getSubpart($this->templateCode, '###RULES###');
1473 $markerArray['###RULES_HEADER###'] = $this->pi_getLL('rules_header', '', TRUE);
1474 $markerArray['###RULES_TEXT###'] = nl2br(trim($this->pi_getLL('rules_text', '', TRUE)));
1475 $substitutedContent = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1476 return $this->cObj->stdWrap($substitutedContent, $this->conf['rules_stdWrap.']);
1477 }
1478 }
1479
1480 /**
1481 * Returns the anchor-links to the sections inside the displayed result rows.
1482 *
1483 * @return string
1484 * @todo Define visibility
1485 */
1486 public function printResultSectionLinks() {
1487 if (count($this->resultSections)) {
1488 $lines = array();
1489 $html = $this->cObj->getSubpart($this->templateCode, '###RESULT_SECTION_LINKS###');
1490 $item = $this->cObj->getSubpart($this->templateCode, '###RESULT_SECTION_LINKS_LINK###');
1491 foreach ($this->resultSections as $id => $dat) {
1492 $markerArray = array();
1493 $aBegin = '<a href="' . htmlspecialchars(($GLOBALS['TSFE']->anchorPrefix . '#anchor_' . md5($id))) . '">';
1494 $aContent = htmlspecialchars((trim($dat[0]) ? trim($dat[0]) : $this->pi_getLL('unnamedSection'))) . ' (' . $dat[1] . ' ' . $this->pi_getLL(($dat[1] > 1 ? 'word_pages' : 'word_page'), '', TRUE) . ')';
1495 $aEnd = '</a>';
1496 $markerArray['###LINK###'] = $aBegin . $aContent . $aEnd;
1497 $links[] = $this->cObj->substituteMarkerArrayCached($item, $markerArray, array(), array());
1498 }
1499 $html = $this->cObj->substituteMarkerArrayCached($html, array('###LINKS###' => implode('', $links)), array(), array());
1500 return '<div' . $this->pi_classParam('sectionlinks') . '>' . $this->cObj->stdWrap($html, $this->conf['sectionlinks_stdWrap.']) . '</div>';
1501 }
1502 }
1503
1504 /**
1505 * Returns the section header of the search result.
1506 *
1507 * @param string ID for the section (used for anchor link)
1508 * @param string Section title with linked wrapped around
1509 * @param integer Number of results in section
1510 * @return string HTML output
1511 * @todo Define visibility
1512 */
1513 public function makeSectionHeader($id, $sectionTitleLinked, $countResultRows) {
1514 $html = $this->cObj->getSubpart($this->templateCode, '###SECTION_HEADER###');
1515 $markerArray['###ANCHOR_URL###'] = 'anchor_' . md5($id);
1516 $markerArray['###SECTION_TITLE###'] = $sectionTitleLinked;
1517 $markerArray['###RESULT_COUNT###'] = $countResultRows;
1518 $markerArray['###RESULT_NAME###'] = $this->pi_getLL('word_page' . ($countResultRows > 1 ? 's' : ''));
1519 $substitutedContent = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1520 return $substitutedContent;
1521 }
1522
1523 /**
1524 * This prints a single result row, including a recursive call for subrows.
1525 *
1526 * @param array Search result row
1527 * @param integer 1=Display only header (for sub-rows!), 2=nothing at all
1528 * @return string HTML code
1529 * @todo Define visibility
1530 */
1531 public function printResultRow($row, $headerOnly = 0) {
1532 // Get template content:
1533 $tmplContent = $this->prepareResultRowTemplateData($row, $headerOnly);
1534 if ($hookObj = $this->hookRequest('printResultRow')) {
1535 return $hookObj->printResultRow($row, $headerOnly, $tmplContent);
1536 } else {
1537 $html = $this->cObj->getSubpart($this->templateCode, '###RESULT_OUTPUT###');
1538 if (!is_array($row['_sub'])) {
1539 $html = $this->cObj->substituteSubpart($html, '###ROW_SUB###', '');
1540 }
1541 if (!$headerOnly) {
1542 $html = $this->cObj->substituteSubpart($html, '###ROW_SHORT###', '');
1543 } elseif ($headerOnly == 1) {
1544 $html = $this->cObj->substituteSubpart($html, '###ROW_LONG###', '');
1545 } elseif ($headerOnly == 2) {
1546 $html = $this->cObj->substituteSubpart($html, '###ROW_SHORT###', '');
1547 $html = $this->cObj->substituteSubpart($html, '###ROW_LONG###', '');
1548 }
1549 if (is_array($tmplContent)) {
1550 foreach ($tmplContent as $k => $v) {
1551 $markerArray['###' . GeneralUtility::strtoupper($k) . '###'] = $v;
1552 }
1553 }
1554 // Description text
1555 $markerArray['###TEXT_ITEM_SIZE###'] = $this->pi_getLL('res_size', '', TRUE);
1556 $markerArray['###TEXT_ITEM_CRDATE###'] = $this->pi_getLL('res_created', '', TRUE);
1557 $markerArray['###TEXT_ITEM_MTIME###'] = $this->pi_getLL('res_modified', '', TRUE);
1558 $markerArray['###TEXT_ITEM_PATH###'] = $this->pi_getLL('res_path', '', TRUE);
1559 $html = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1560 // If there are subrows (eg. subpages in a PDF-file or if a duplicate page is selected due to user-login (phash_grouping))
1561 if (is_array($row['_sub'])) {
1562 if ($this->multiplePagesType($row['item_type'])) {
1563 $html = str_replace('###TEXT_ROW_SUB###', $this->pi_getLL('res_otherMatching', '', TRUE), $html);
1564 foreach ($row['_sub'] as $subRow) {
1565 $html .= $this->printResultRow($subRow, 1);
1566 }
1567 } else {
1568 $markerArray['###TEXT_ROW_SUB###'] = $this->pi_getLL('res_otherMatching', '', TRUE);
1569 $html = str_replace('###TEXT_ROW_SUB###', $this->pi_getLL('res_otherPageAsWell', '', TRUE), $html);
1570 }
1571 }
1572 return $html;
1573 }
1574 }
1575
1576 /**
1577 * Returns a results browser
1578 *
1579 * @param boolean Show result count
1580 * @param string String appended to "displaying results..." notice.
1581 * @param string String appended after section "displaying results...
1582 * @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!
1583 * @return string HTML output
1584 * @todo Define visibility
1585 */
1586 public function pi_list_browseresults($showResultCount = 1, $addString = '', $addPart = '', $freeIndexUid = -1) {
1587 // Initializing variables:
1588 $pointer = $this->piVars['pointer'];
1589 $count = $this->internal['res_count'];
1590 $results_at_a_time = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($this->internal['results_at_a_time'], 1, 1000);
1591 $maxPages = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($this->internal['maxPages'], 1, 100);
1592 $pageCount = ceil($count / $results_at_a_time);
1593 $sTables = '';
1594 if ($pageCount > 1) {
1595 // only show the result browser if more than one page is needed
1596 $pointer = intval($pointer);
1597 $links = array();
1598 // Make browse-table/links:
1599 if ($pointer > 0) {
1600 // all pages after the 1st one
1601 $links[] = '<li>' . $this->makePointerSelector_link($this->pi_getLL('pi_list_browseresults_prev', '< Previous', TRUE), ($pointer - 1), $freeIndexUid) . '</li>';
1602 }
1603 for ($a = 0; $a < $pageCount; $a++) {
1604 $min = max(0, $pointer + 1 - ceil($maxPages / 2));
1605 $max = $min + $maxPages;
1606 if ($max > $pageCount) {
1607 $min = $min - ($max - $pageCount);
1608 }
1609 if ($a >= $min && $a < $max) {
1610 if ($a == $pointer) {
1611 $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>';
1612 } else {
1613 $links[] = '<li>' . $this->makePointerSelector_link(trim(($this->pi_getLL('pi_list_browseresults_page', 'Page', TRUE) . ' ' . ($a + 1))), $a, $freeIndexUid) . '</li>';
1614 }
1615 }
1616 }
1617 if ($pointer + 1 < $pageCount) {
1618 $links[] = '<li>' . $this->makePointerSelector_link($this->pi_getLL('pi_list_browseresults_next', 'Next >', TRUE), ($pointer + 1), $freeIndexUid) . '</li>';
1619 }
1620 }
1621 $pR1 = $pointer * $results_at_a_time + 1;
1622 $pR2 = $pointer * $results_at_a_time + $results_at_a_time;
1623 if (is_array($links)) {
1624 $addPart .= '
1625 <ul class="browsebox">
1626 ' . implode('', $links) . '
1627 </ul>';
1628 }
1629 $label = str_replace(
1630 array('###TAG_BEGIN###', '###TAG_END###'),
1631 array('<strong>', '</strong>'),
1632 $this->pi_getLL('pi_list_browseresults_display', 'Displaying results ###TAG_BEGIN###%s to %s###TAG_END### out of ###TAG_BEGIN###%s###TAG_END###')
1633 );
1634 $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>';
1635 return $sTables;
1636 }
1637
1638 /***********************************
1639 *
1640 * Support functions for HTML output (with a minimum of fixed markup)
1641 *
1642 ***********************************/
1643 /**
1644 * Preparing template data for the result row output
1645 *
1646 * @param array Result row
1647 * @param boolean If set, display only header of result (for sub-results)
1648 * @return array Array with data to insert in result row template
1649 * @todo Define visibility
1650 */
1651 public function prepareResultRowTemplateData($row, $headerOnly) {
1652 // Initialize:
1653 $specRowConf = $this->getSpecialConfigForRow($row);
1654 $CSSsuffix = $specRowConf['CSSsuffix'] ? '-' . $specRowConf['CSSsuffix'] : '';
1655 // If external media, link to the media-file instead.
1656 if ($row['item_type']) {
1657 // External media
1658 if ($row['show_resume']) {
1659 // Can link directly.
1660 $targetAttribute = '';
1661 if ($GLOBALS['TSFE']->config['config']['fileTarget']) {
1662 $targetAttribute = ' target="' . htmlspecialchars($GLOBALS['TSFE']->config['config']['fileTarget']) . '"';
1663 }
1664 $title = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($this->makeTitle($row)) . '</a>';
1665 } else {
1666 // Suspicious, so linking to page instead...
1667 $copy_row = $row;
1668 unset($copy_row['cHashParams']);
1669 $title = $this->linkPage($row['page_id'], htmlspecialchars($this->makeTitle($row)), $copy_row);
1670 }
1671 } else {
1672 // Else the page:
1673 // Prepare search words for markup in content:
1674 if ($this->conf['forwardSearchWordsInResultLink']) {
1675 $markUpSwParams = array('no_cache' => 1);
1676 foreach ($this->sWArr as $d) {
1677 $markUpSwParams['sword_list'][] = $d['sword'];
1678 }
1679 } else {
1680 $markUpSwParams = array();
1681 }
1682 $title = $this->linkPage($row['data_page_id'], htmlspecialchars($this->makeTitle($row)), $row, $markUpSwParams);
1683 }
1684 $tmplContent = array();
1685 $tmplContent['title'] = $title;
1686 $tmplContent['result_number'] = $this->conf['show.']['resultNumber'] ? $row['result_number'] . ': ' : '&nbsp;';
1687 $tmplContent['icon'] = $this->makeItemTypeIcon($row['item_type'], '', $specRowConf);
1688 $tmplContent['rating'] = $this->makeRating($row);
1689 $tmplContent['description'] = $this->makeDescription($row, $this->piVars['extResume'] && !$headerOnly ? 0 : 1);
1690 $tmplContent = $this->makeInfo($row, $tmplContent);
1691 $tmplContent['access'] = $this->makeAccessIndication($row['page_id']);
1692 $tmplContent['language'] = $this->makeLanguageIndication($row);
1693 $tmplContent['CSSsuffix'] = $CSSsuffix;
1694 // Post processing with hook.
1695 if ($hookObj = $this->hookRequest('prepareResultRowTemplateData_postProc')) {
1696 $tmplContent = $hookObj->prepareResultRowTemplateData_postProc($tmplContent, $row, $headerOnly);
1697 }
1698 return $tmplContent;
1699 }
1700
1701 /**
1702 * Returns a string that tells which search words are searched for.
1703 *
1704 * @param array Array of search words
1705 * @return string HTML telling what is searched for.
1706 * @todo Define visibility
1707 */
1708 public function tellUsWhatIsSeachedFor($sWArr) {
1709 // Init:
1710 $searchingFor = '';
1711 $c = 0;
1712 // Traverse search words:
1713 foreach ($sWArr as $k => $v) {
1714 if ($c) {
1715 switch ($v['oper']) {
1716 case 'OR':
1717 $searchingFor .= ' ' . $this->pi_getLL('searchFor_or', '', TRUE) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1718 break;
1719 case 'AND NOT':
1720 $searchingFor .= ' ' . $this->pi_getLL('searchFor_butNot', '', TRUE) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1721 break;
1722 default:
1723 // AND...
1724 $searchingFor .= ' ' . $this->pi_getLL('searchFor_and', '', TRUE) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1725 }
1726 } else {
1727 $searchingFor = $this->pi_getLL('searchFor', '', TRUE) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1728 }
1729 $c++;
1730 }
1731 return $searchingFor;
1732 }
1733
1734 /**
1735 * Wraps the search words in the search-word list display (from ->tellUsWhatIsSeachedFor())
1736 *
1737 * @param string search word to wrap (in local charset!)
1738 * @return string Search word wrapped in <span> tag.
1739 * @todo Define visibility
1740 */
1741 public function wrapSW($str) {
1742 return '"<span' . $this->pi_classParam('sw') . '>' . htmlspecialchars($str) . '</span>"';
1743 }
1744
1745 /**
1746 * Makes a selector box
1747 *
1748 * @param string Name of selector box
1749 * @param string Current value
1750 * @param array Array of options in the selector box (value => label pairs)
1751 * @return string HTML of selector box
1752 * @todo Define visibility
1753 */
1754 public function renderSelectBox($name, $value, $optValues) {
1755 if (is_array($optValues)) {
1756 $opt = array();
1757 $isSelFlag = 0;
1758 foreach ($optValues as $k => $v) {
1759 $sel = !strcmp($k, $value) ? ' selected="selected"' : '';
1760 if ($sel) {
1761 $isSelFlag++;
1762 }
1763 $opt[] = '<option value="' . htmlspecialchars($k) . '"' . $sel . '>' . htmlspecialchars($v) . '</option>';
1764 }
1765 return '<select name="' . $name . '">' . implode('', $opt) . '</select>';
1766 }
1767 }
1768
1769 /**
1770 * Used to make the link for the result-browser.
1771 * Notice how the links must resubmit the form after setting the new pointer-value in a hidden formfield.
1772 *
1773 * @param string String to wrap in <a> tag
1774 * @param integer Pointer value
1775 * @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!
1776 * @return string Input string wrapped in <a> tag with onclick event attribute set.
1777 * @todo Define visibility
1778 */
1779 public function makePointerSelector_link($str, $p, $freeIndexUid) {
1780 $onclick = 'document.getElementById(\'' . $this->prefixId . '_pointer\').value=\'' . $p . '\';' . 'document.getElementById(\'' . $this->prefixId . '_freeIndexUid\').value=\'' . rawurlencode($freeIndexUid) . '\';' . 'document.getElementById(\'' . $this->prefixId . '\').submit();return false;';
1781 return '<a href="#" onclick="' . htmlspecialchars($onclick) . '">' . $str . '</a>';
1782 }
1783
1784 /**
1785 * Return icon for file extension
1786 *
1787 * @param string File extension / item type
1788 * @param string Title attribute value in icon.
1789 * @param array TypoScript configuration specifically for search result.
1790 * @return string <img> tag for icon
1791 * @todo Define visibility
1792 */
1793 public function makeItemTypeIcon($it, $alt = '', $specRowConf) {
1794 // Build compound key if item type is 0, iconRendering is not used
1795 // and specConfs.[pid].pageIcon was set in TS
1796 if ($it === '0' && $specRowConf['_pid'] && is_array($specRowConf['pageIcon.']) && !is_array($this->conf['iconRendering.'])) {
1797 $it .= ':' . $specRowConf['_pid'];
1798 }
1799 if (!isset($this->iconFileNameCache[$it])) {
1800 $this->iconFileNameCache[$it] = '';
1801 // If TypoScript is used to render the icon:
1802 if (is_array($this->conf['iconRendering.'])) {
1803 $this->cObj->setCurrentVal($it);
1804 $this->iconFileNameCache[$it] = $this->cObj->cObjGetSingle($this->conf['iconRendering'], $this->conf['iconRendering.']);
1805 } else {
1806 // Default creation / finding of icon:
1807 $icon = '';
1808 if ($it === '0' || substr($it, 0, 2) == '0:') {
1809 if (is_array($specRowConf['pageIcon.'])) {
1810 $this->iconFileNameCache[$it] = $this->cObj->IMAGE($specRowConf['pageIcon.']);
1811 } else {
1812 $icon = 'EXT:indexed_search/pi/res/pages.gif';
1813 }
1814 } elseif ($this->external_parsers[$it]) {
1815 $icon = $this->external_parsers[$it]->getIcon($it);
1816 }
1817 if ($icon) {
1818 $fullPath = GeneralUtility::getFileAbsFileName($icon);
1819 if ($fullPath) {
1820 $info = @getimagesize($fullPath);
1821 $iconPath = \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix($fullPath);
1822 $this->iconFileNameCache[$it] = is_array($info) ? '<img src="' . $iconPath . '" ' . $info[3] . ' title="' . htmlspecialchars($alt) . '" alt="" />' : '';
1823 }
1824 }
1825 }
1826 }
1827 return $this->iconFileNameCache[$it];
1828 }
1829
1830 /**
1831 * Return the rating-HTML code for the result row. This makes use of the $this->firstRow
1832 *
1833 * @param array Result row array
1834 * @return string String showing ranking value
1835 * @todo Define visibility
1836 */
1837 public function makeRating($row) {
1838 switch ((string) $this->piVars['order']) {
1839 case 'rank_count':
1840 // Number of occurencies on page
1841 return $row['order_val'] . ' ' . $this->pi_getLL('maketitle_matches');
1842 break;
1843 case 'rank_first':
1844 // Close to top of page
1845 return ceil(\TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange((255 - $row['order_val']), 1, 255) / 255 * 100) . '%';
1846 break;
1847 case 'rank_flag':
1848 // Based on priority assigned to <title> / <meta-keywords> / <meta-description> / <body>
1849 if ($this->firstRow['order_val2']) {
1850 $base = $row['order_val1'] * 256;
1851 // (3 MSB bit, 224 is highest value of order_val1 currently)
1852 $freqNumber = $row['order_val2'] / $this->firstRow['order_val2'] * pow(2, 12);
1853 // 15-3 MSB = 12
1854 $total = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($base + $freqNumber, 0, 32767);
1855 return ceil(log($total) / log(32767) * 100) . '%';
1856 }
1857 break;
1858 case 'rank_freq':
1859 // Based on frequency
1860 $max = 10000;
1861 $total = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($row['order_val'], 0, $max);
1862 return ceil(log($total) / log($max) * 100) . '%';
1863 break;
1864 case 'crdate':
1865 // Based on creation date
1866 return $this->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_crdate'], 0);
1867 break;
1868 case 'mtime':
1869 // Based on modification time
1870 return $this->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_mtime'], 0);
1871 break;
1872 default:
1873 // fx. title
1874 return '&nbsp;';
1875 }
1876 }
1877
1878 /**
1879 * Returns the resume for the search-result.
1880 *
1881 * @param array Search result row
1882 * @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.
1883 * @param integer String length
1884 * @return string HTML string ...
1885 * @todo Define visibility
1886 */
1887 public function makeDescription($row, $noMarkup = 0, $lgd = 180) {
1888 if ($row['show_resume']) {
1889 if (!$noMarkup) {
1890 $markedSW = '';
1891 if (\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_fulltext')) {
1892 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'index_fulltext', 'phash=' . intval($row['phash']));
1893 } else {
1894 $res = FALSE;
1895 }
1896 if ($res) {
1897 if ($ftdrow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
1898 // Cut HTTP references after some length
1899 $content = preg_replace('/(http:\\/\\/[^ ]{60})([^ ]+)/i', '$1...', $ftdrow['fulltextdata']);
1900 $markedSW = $this->markupSWpartsOfString($content);
1901 }
1902 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1903 }
1904 }
1905 if (!trim($markedSW)) {
1906 $outputStr = $GLOBALS['TSFE']->csConvObj->crop('utf-8', $row['item_description'], $lgd);
1907 $outputStr = htmlspecialchars($outputStr);
1908 }
1909 $output = $this->utf8_to_currentCharset($outputStr ? $outputStr : $markedSW);
1910 } else {
1911 $output = '<span class="noResume">' . $this->pi_getLL('res_noResume', '', TRUE) . '</span>';
1912 }
1913 return $output;
1914 }
1915
1916 /**
1917 * Marks up the search words from $this->sWarr in the $str with a color.
1918 *
1919 * @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.
1920 * @return string Processed content.
1921 * @todo Define visibility
1922 */
1923 public function markupSWpartsOfString($str) {
1924 // Init:
1925 $str = str_replace('&nbsp;', ' ', \TYPO3\CMS\Core\Html\HtmlParser::bidir_htmlspecialchars($str, -1));
1926 $str = preg_replace('/\\s\\s+/', ' ', $str);
1927 $swForReg = array();
1928 // Prepare search words for regex:
1929 foreach ($this->sWArr as $d) {
1930 $swForReg[] = preg_quote($d['sword'], '/');
1931 }
1932 $regExString = '(' . implode('|', $swForReg) . ')';
1933 // Split and combine:
1934 $parts = preg_split('/' . $regExString . '/i', ' ' . $str . ' ', 20000, PREG_SPLIT_DELIM_CAPTURE);
1935 // Constants:
1936 $summaryMax = 300;
1937 $postPreLgd = 60;
1938 $postPreLgd_offset = 5;
1939 $divider = ' ... ';
1940 $occurencies = (count($parts) - 1) / 2;
1941 if ($occurencies) {
1942 $postPreLgd = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($summaryMax / $occurencies, $postPreLgd, $summaryMax / 2);
1943 }
1944 // Variable:
1945 $summaryLgd = 0;
1946 $output = array();
1947 // Shorten in-between strings:
1948 foreach ($parts as $k => $strP) {
1949 if ($k % 2 == 0) {
1950 // Find length of the summary part:
1951 $strLen = $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $parts[$k]);
1952 $output[$k] = $parts[$k];
1953 // Possibly shorten string:
1954 if (!$k) {
1955 // First entry at all (only cropped on the frontside)
1956 if ($strLen > $postPreLgd) {
1957 $output[$k] = $divider . preg_replace('/^[^[:space:]]+[[:space:]]/', '', $GLOBALS['TSFE']->csConvObj->crop('utf-8', $parts[$k], -($postPreLgd - $postPreLgd_offset)));
1958 }
1959 } elseif ($summaryLgd > $summaryMax || !isset($parts[($k + 1)])) {
1960 // In case summary length is exceed OR if there are no more entries at all:
1961 if ($strLen > $postPreLgd) {
1962 $output[$k] = preg_replace('/[[:space:]][^[:space:]]+$/', '', $GLOBALS['TSFE']->csConvObj->crop('utf-8', $parts[$k], ($postPreLgd - $postPreLgd_offset))) . $divider;
1963 }
1964 } else {
1965 // In-between search words:
1966 if ($strLen > $postPreLgd * 2) {
1967 $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)));
1968 }
1969 }
1970 $summaryLgd += $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $output[$k]);
1971 // Protect output:
1972 $output[$k] = htmlspecialchars($output[$k]);
1973 // If summary lgd is exceed, break the process:
1974 if ($summaryLgd > $summaryMax) {
1975 break;
1976 }
1977 } else {
1978 $summaryLgd += $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $strP);
1979 $output[$k] = '<strong class="tx-indexedsearch-redMarkup">' . htmlspecialchars($parts[$k]) . '</strong>';
1980 }
1981 }
1982 // Return result:
1983 return implode('', $output);
1984 }
1985
1986 /**
1987 * Returns the title of the search result row
1988 *
1989 * @param array Result row
1990 * @return string Title from row
1991 * @todo Define visibility
1992 */
1993 public function makeTitle($row) {
1994 $add = '';
1995 if ($this->multiplePagesType($row['item_type'])) {
1996 $dat = unserialize($row['cHashParams']);
1997 $pp = explode('-', $dat['key']);
1998 if ($pp[0] != $pp[1]) {
1999 $add = ', ' . $this->pi_getLL('word_pages') . ' ' . $dat['key'];
2000 } else {
2001 $add = ', ' . $this->pi_getLL('word_page') . ' ' . $pp[0];
2002 }
2003 }
2004 $outputString = $GLOBALS['TSFE']->csConvObj->crop('utf-8', $row['item_title'], 50, '...');
2005 return $this->utf8_to_currentCharset($outputString) . $add;
2006 }
2007
2008 /**
2009 * Returns the info-string in the bottom of the result-row display (size, dates, path)
2010 *
2011 * @param array Result row
2012 * @param array Template array to modify
2013 * @return array Modified template array
2014 * @todo Define visibility
2015 */
2016 public function makeInfo($row, $tmplArray) {
2017 $tmplArray['size'] = GeneralUtility::formatSize($row['item_size']);
2018 $tmplArray['created'] = $this->formatCreatedDate($row['item_crdate']);
2019 $tmplArray['modified'] = $this->formatModifiedDate($row['item_mtime']);
2020 $pathId = $row['data_page_id'] ? $row['data_page_id'] : $row['page_id'];
2021 $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
2022 $pI = parse_url($row['data_filename']);
2023 if ($pI['scheme']) {
2024 $targetAttribute = '';
2025 if ($GLOBALS['TSFE']->config['config']['fileTarget']) {
2026 $targetAttribute = ' target="' . htmlspecialchars($GLOBALS['TSFE']->config['config']['fileTarget']) . '"';
2027 }
2028 $tmplArray['path'] = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($row['data_filename']) . '</a>';
2029 } else {
2030 $pathStr = htmlspecialchars($this->getPathFromPageId($pathId, $pathMP));
2031 $tmplArray['path'] = $this->linkPage($pathId, $pathStr, array(
2032 'cHashParams' => $row['cHashParams'],
2033 'data_page_type' => $row['data_page_type'],
2034 'data_page_mp' => $pathMP,
2035 'sys_language_uid' => $row['sys_language_uid']
2036 ));
2037 }
2038 return $tmplArray;
2039 }
2040
2041 /**
2042 * Returns configuration from TypoScript for result row based on ID / location in page tree!
2043 *
2044 * @param array Result row
2045 * @return array Configuration array
2046 * @todo Define visibility
2047 */
2048 public function getSpecialConfigForRow($row) {
2049 $pathId = $row['data_page_id'] ? $row['data_page_id'] : $row['page_id'];
2050 $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
2051 $rl = $this->getRootLine($pathId, $pathMP);
2052 $specConf = $this->conf['specConfs.']['0.'];
2053 if (is_array($rl)) {
2054 foreach ($rl as $dat) {
2055 if (is_array($this->conf['specConfs.'][$dat['uid'] . '.'])) {
2056 $specConf = $this->conf['specConfs.'][$dat['uid'] . '.'];
2057 $specConf['_pid'] = $dat['uid'];
2058 break;
2059 }
2060 }
2061 }
2062 return $specConf;
2063 }
2064
2065 /**
2066 * Returns the HTML code for language indication.
2067 *
2068 * @param array Result row
2069 * @return string HTML code for result row.
2070 * @todo Define visibility
2071 */
2072 public function makeLanguageIndication($row) {
2073 // If search result is a TYPO3 page:
2074 if ((string) $row['item_type'] === '0') {
2075 // If TypoScript is used to render the flag:
2076 if (is_array($this->conf['flagRendering.'])) {
2077 $this->cObj->setCurrentVal($row['sys_language_uid']);
2078 return $this->cObj->cObjGetSingle($this->conf['flagRendering'], $this->conf['flagRendering.']);
2079 } else {
2080 // ... otherwise, get flag from sys_language record:
2081 // Get sys_language record
2082 $rowDat = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('*', 'sys_language', 'uid=' . intval($row['sys_language_uid']) . ' ' . $this->cObj->enableFields('sys_language'));
2083 // Flag code:
2084 $flag = $rowDat['flag'];
2085 if ($flag) {
2086 // FIXME not all flags from typo3/gfx/flags are available in media/flags/
2087 $file = \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix(PATH_tslib) . 'media/flags/flag_' . $flag;
2088 $imgInfo = @getimagesize((PATH_site . $file));
2089 if (is_array($imgInfo)) {
2090 $output = '<img src="' . $file . '" ' . $imgInfo[3] . ' title="' . htmlspecialchars($rowDat['title']) . '" alt="' . htmlspecialchars($rowDat['title']) . '" />';
2091 return $output;
2092 }
2093 }
2094 }
2095 }
2096 return '&nbsp;';
2097 }
2098
2099 /**
2100 * Returns the HTML code for the locking symbol.
2101 * NOTICE: Requires a call to ->getPathFromPageId() first in order to work (done in ->makeInfo() by calling that first)
2102 *
2103 * @param integer Page id for which to find answer
2104 * @return string <img> tag if access is limited.
2105 * @todo Define visibility
2106 */
2107 public function makeAccessIndication($id) {
2108 if (is_array($this->fe_groups_required[$id]) && count($this->fe_groups_required[$id])) {
2109 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', '', TRUE), implode(',', array_unique($this->fe_groups_required[$id]))) . '" alt="" />';
2110 }
2111 }
2112
2113 /**
2114 * Links the $str to page $id
2115 *
2116 * @param integer Page id
2117 * @param string Title String to link
2118 * @param array Result row
2119 * @param array Additional parameters for marking up seach words
2120 * @return string <A> tag wrapped title string.
2121 * @todo Define visibility
2122 */
2123 public function linkPage($id, $str, $row = array(), $markUpSwParams = array()) {
2124 // Parameters for link:
2125 $urlParameters = (array) unserialize($row['cHashParams']);
2126 // Add &type and &MP variable:
2127 if ($row['data_page_type']) {
2128 $urlParameters['type'] = $row['data_page_type'];
2129 }
2130 if ($row['data_page_mp']) {
2131 $urlParameters['MP'] = $row['data_page_mp'];
2132 }
2133 if ($row['sys_language_uid']) {
2134 $urlParameters['L'] = $row['sys_language_uid'];
2135 }
2136 // markup-GET vars:
2137 $urlParameters = array_merge($urlParameters, $markUpSwParams);
2138 // 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...
2139 if (!is_array($this->domain_records[$id])) {
2140 $this->getPathFromPageId($id);
2141 }
2142 // If external domain, then link to that:
2143 if (count($this->domain_records[$id])) {
2144 reset($this->domain_records[$id]);
2145 $firstDom = current($this->domain_records[$id]);
2146 $scheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://';
2147 $addParams = '';
2148 if (is_array($urlParameters)) {
2149 if (count($urlParameters)) {
2150 $addParams .= GeneralUtility::implodeArrayForUrl('', $urlParameters);
2151 }
2152 }
2153 if ($target = $this->conf['search.']['detect_sys_domain_records.']['target']) {
2154 $target = ' target="' . $target . '"';
2155 }
2156 return '<a href="' . htmlspecialchars(($scheme . $firstDom . '/index.php?id=' . $id . $addParams)) . '"' . $target . '>' . htmlspecialchars($str) . '</a>';
2157 } else {
2158 return $this->pi_linkToPage($str, $id, $this->conf['result_link_target'], $urlParameters);
2159 }
2160 }
2161
2162 /**
2163 * Returns the path to the page $id
2164 *
2165 * @param integer Page ID
2166 * @param string MP variable content.
2167 * @return string Root line for result.
2168 * @todo Define visibility
2169 */
2170 public function getRootLine($id, $pathMP = '') {
2171 $identStr = $id . '|' . $pathMP;
2172 if (!isset($this->cache_path[$identStr])) {
2173 $this->cache_rl[$identStr] = $GLOBALS['TSFE']->sys_page->getRootLine($id, $pathMP);
2174 }
2175 return $this->cache_rl[$identStr];
2176 }
2177
2178 /**
2179 * Gets the first sys_domain record for the page, $id
2180 *
2181 * @param integer Page id
2182 * @return string Domain name
2183 * @todo Define visibility
2184 */
2185 public function getFirstSysDomainRecordForPage($id) {
2186 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('domainName', 'sys_domain', 'pid=' . intval($id) . $this->cObj->enableFields('sys_domain'), '', 'sorting');
2187 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
2188 return rtrim($row['domainName'], '/');
2189 }
2190
2191 /**
2192 * Returns the path to the page $id
2193 *
2194 * @param integer Page ID
2195 * @param string MP variable content
2196 * @return string Path
2197 * @todo Define visibility
2198 */
2199 public function getPathFromPageId($id, $pathMP = '') {
2200 $identStr = $id . '|' . $pathMP;
2201 if (!isset($this->cache_path[$identStr])) {
2202 $this->fe_groups_required[$id] = array();
2203 $this->domain_records[$id] = array();
2204 $rl = $this->getRootLine($id, $pathMP);
2205 $hitRoot = 0;
2206 $path = '';
2207 if (is_array($rl) && count($rl)) {
2208 foreach ($rl as $k => $v) {
2209 // Check fe_user
2210 if ($v['fe_group'] && ($v['uid'] == $id || $v['extendToSubpages'])) {
2211 $this->fe_groups_required[$id][] = $v['fe_group'];
2212 }
2213 // Check sys_domain.
2214 if ($this->conf['search.']['detect_sys_domain_records']) {
2215 $sysDName = $this->getFirstSysDomainRecordForPage($v['uid']);
2216 if ($sysDName) {
2217 $this->domain_records[$id][] = $sysDName;
2218 // Set path accordingly:
2219 $path = $sysDName . $path;
2220 break;
2221 }
2222 }
2223 // Stop, if we find that the current id is the current root page.
2224 if ($v['uid'] == $GLOBALS['TSFE']->config['rootLine'][0]['uid']) {
2225 break;
2226 }
2227 $path = '/' . $v['title'] . $path;
2228 }
2229 }
2230 $this->cache_path[$identStr] = $path;
2231 if (is_array($this->conf['path_stdWrap.'])) {
2232 $this->cache_path[$identStr] = $this->cObj->stdWrap($this->cache_path[$identStr], $this->conf['path_stdWrap.']);
2233 }
2234 }
2235 return $this->cache_path[$identStr];
2236 }
2237
2238 /**
2239 * Return the menu of pages used for the selector.
2240 *
2241 * @param integer Page ID for which to return menu
2242 * @return array Menu items (for making the section selector box)
2243 * @todo Define visibility
2244 */
2245 public function getMenu($id) {
2246 if ($this->conf['show.']['LxALLtypes']) {
2247 $output = array();
2248 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('title,uid', 'pages', 'pid=' . intval($id) . $this->cObj->enableFields('pages'), '', 'sorting');
2249 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
2250 $output[$row['uid']] = $GLOBALS['TSFE']->sys_page->getPageOverlay($row);
2251 }
2252 $GLOBALS['TYPO3_DB']->sql_free_result($res);
2253 return $output;
2254 } else {
2255 return $GLOBALS['TSFE']->sys_page->getMenu($id);
2256 }
2257 }
2258
2259 /**
2260 * Returns if an item type is a multipage item type
2261 *
2262 * @param string Item type
2263 * @return boolean TRUE if multipage capable
2264 * @todo Define visibility
2265 */
2266 public function multiplePagesType($item_type) {
2267 return is_object($this->external_parsers[$item_type]) && $this->external_parsers[$item_type]->isMultiplePageExtension($item_type);
2268 }
2269
2270 /**
2271 * Converts the input string from utf-8 to the backend charset.
2272 *
2273 * @param string String to convert (utf-8)
2274 * @return string Converted string (backend charset if different from utf-8)
2275 * @todo Define visibility
2276 */
2277 public function utf8_to_currentCharset($str) {
2278 return $GLOBALS['TSFE']->csConv($str, 'utf-8');
2279 }
2280
2281 /**
2282 * Returns an object reference to the hook object if any
2283 *
2284 * @param string Name of the function you want to call / hook key
2285 * @return object Hook object, if any. Otherwise NULL.
2286 * @todo Define visibility
2287 */
2288 public function hookRequest($functionName) {
2289 global $TYPO3_CONF_VARS;
2290 // Hook: menuConfig_preProcessModMenu
2291 if ($TYPO3_CONF_VARS['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
2292 $hookObj = GeneralUtility::getUserObj($TYPO3_CONF_VARS['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
2293 if (method_exists($hookObj, $functionName)) {
2294 $hookObj->pObj = $this;
2295 return $hookObj;
2296 }
2297 }
2298 }
2299
2300 /**
2301 * Obtains the URL of the search target page
2302 *
2303 * @return string
2304 */
2305 protected function getSearchFormActionURL() {
2306 $targetUrlPid = $this->getSearchFormActionPidFromTS();
2307 if ($targetUrlPid == 0) {
2308 $targetUrlPid = $GLOBALS['TSFE']->id;
2309 }
2310 return $this->pi_getPageLink($targetUrlPid, $GLOBALS['TSFE']->sPre);
2311 }
2312
2313 /**
2314 * Obtains search form target pid from the TypoScript configuration
2315 *
2316 * @return integer
2317 */
2318 protected function getSearchFormActionPidFromTS() {
2319 $result = 0;
2320 if (isset($this->conf['search.']['targetPid']) || isset($this->conf['search.']['targetPid.'])) {
2321 if (is_array($this->conf['search.']['targetPid.'])) {
2322 $result = $this->cObj->stdWrap($this->conf['search.']['targetPid'], $this->conf['search.']['targetPid.']);
2323 } else {
2324 $result = $this->conf['search.']['targetPid'];
2325 }
2326 $result = intval($result);
2327 }
2328 return $result;
2329 }
2330
2331 /**
2332 * Formats date as 'created' date
2333 *
2334 * @param integer $date
2335 * @return string
2336 */
2337 protected function formatCreatedDate($date) {
2338 $defaultFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
2339 return $this->formatDate($date, 'created', $defaultFormat);
2340 }
2341
2342 /**
2343 * Formats date as 'modified' date
2344 *
2345 * @param integer $date
2346 * @return string
2347 */
2348 protected function formatModifiedDate($date) {
2349 $defaultFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
2350 return $this->formatDate($date, 'modified', $defaultFormat);
2351 }
2352
2353 /**
2354 * Formats the date using format string from TypoScript or default format
2355 * if TypoScript format is not set
2356 *
2357 * @param integer $date
2358 * @param string $tsKey
2359 * @param string $defaultFormat
2360 * @return string
2361 */
2362 protected function formatDate($date, $tsKey, $defaultFormat) {
2363 $strftimeFormat = $this->conf['dateFormat.'][$tsKey];
2364 if ($strftimeFormat) {
2365 $result = strftime($strftimeFormat, $date);
2366 } else {
2367 $result = date($defaultFormat, $date);
2368 }
2369 return $result;
2370 }
2371
2372 }