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