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