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