[TASK] Unify TypoScript-related helper methods
[Packages/TYPO3.CMS.git] / typo3 / sysext / indexed_search / Classes / Controller / SearchController.php
1 <?php
2 namespace TYPO3\CMS\IndexedSearch\Controller;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Charset\CharsetConverter;
18 use TYPO3\CMS\Core\Database\Connection;
19 use TYPO3\CMS\Core\Database\ConnectionPool;
20 use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
21 use TYPO3\CMS\Core\Html\HtmlParser;
22 use TYPO3\CMS\Core\Utility\GeneralUtility;
23 use TYPO3\CMS\Core\Utility\MathUtility;
24 use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
25
26 /**
27 * Index search frontend
28 *
29 * Creates a search form for indexed search. Indexing must be enabled
30 * for this to make sense.
31 */
32 class SearchController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
33 {
34 /**
35 * previously known as $this->piVars['sword']
36 *
37 * @var string
38 */
39 protected $sword = null;
40
41 /**
42 * @var array
43 */
44 protected $searchWords = [];
45
46 /**
47 * @var array
48 */
49 protected $searchData;
50
51 /**
52 * This is the id of the site root.
53 * This value may be a comma separated list of integer (prepared for this)
54 * Root-page PIDs to search in (rl0 field where clause, see initialize() function)
55 *
56 * If this value is set to less than zero (eg. -1) searching will happen
57 * in ALL of the page tree with no regard to branches at all.
58 * @var int|string
59 */
60 protected $searchRootPageIdList = 0;
61
62 /**
63 * @var int
64 */
65 protected $defaultResultNumber = 10;
66
67 /**
68 * @var int[]
69 */
70 protected $availableResultsNumbers = [];
71
72 /**
73 * Search repository
74 *
75 * @var \TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository
76 */
77 protected $searchRepository = null;
78
79 /**
80 * Lexer object
81 *
82 * @var \TYPO3\CMS\IndexedSearch\Lexer
83 */
84 protected $lexerObj;
85
86 /**
87 * External parser objects
88 * @var array
89 */
90 protected $externalParsers = [];
91
92 /**
93 * Will hold the first row in result - used to calculate relative hit-ratings.
94 *
95 * @var array
96 */
97 protected $firstRow = [];
98
99 /**
100 * sys_domain records
101 *
102 * @var array
103 */
104 protected $domainRecords = [];
105
106 /**
107 * Required fe_groups memberships for display of a result.
108 *
109 * @var array
110 */
111 protected $requiredFrontendUsergroups = [];
112
113 /**
114 * Page tree sections for search result.
115 *
116 * @var array
117 */
118 protected $resultSections = [];
119
120 /**
121 * Caching of page path
122 *
123 * @var array
124 */
125 protected $pathCache = [];
126
127 /**
128 * Storage of icons
129 *
130 * @var array
131 */
132 protected $iconFileNameCache = [];
133
134 /**
135 * Indexer configuration, coming from $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['indexed_search']
136 *
137 * @var array
138 */
139 protected $indexerConfig = [];
140
141 /**
142 * Flag whether metaphone search should be enabled
143 *
144 * @var bool
145 */
146 protected $enableMetaphoneSearch = false;
147
148 /**
149 * @var \TYPO3\CMS\Core\TypoScript\TypoScriptService
150 */
151 protected $typoScriptService;
152
153 /**
154 * @var CharsetConverter
155 */
156 protected $charsetConverter;
157
158 /**
159 * @param \TYPO3\CMS\Core\TypoScript\TypoScriptService $typoScriptService
160 */
161 public function injectTypoScriptService(\TYPO3\CMS\Core\TypoScript\TypoScriptService $typoScriptService)
162 {
163 $this->typoScriptService = $typoScriptService;
164 }
165
166 /**
167 * sets up all necessary object for searching
168 *
169 * @param array $searchData The incoming search parameters
170 * @return array Search parameters
171 */
172 public function initialize($searchData = [])
173 {
174 $this->charsetConverter = GeneralUtility::makeInstance(CharsetConverter::class);
175 if (!is_array($searchData)) {
176 $searchData = [];
177 }
178
179 // check if TypoScript is loaded
180 if (!isset($this->settings['results'])) {
181 $this->redirect('noTypoScript');
182 }
183
184 // Sets availableResultsNumbers - has to be called before request settings are read to avoid DoS attack
185 $this->availableResultsNumbers = array_filter(GeneralUtility::intExplode(',', $this->settings['blind']['numberOfResults']));
186
187 // Sets default result number if at least one availableResultsNumbers exists
188 if (isset($this->availableResultsNumbers[0])) {
189 $this->defaultResultNumber = $this->availableResultsNumbers[0];
190 }
191
192 $this->loadSettings();
193
194 // setting default values
195 if (is_array($this->settings['defaultOptions'])) {
196 $searchData = array_merge($this->settings['defaultOptions'], $searchData);
197 }
198 // Indexer configuration from Extension Manager interface:
199 $this->indexerConfig = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['indexed_search'], ['allowed_classes' => false]);
200 $this->enableMetaphoneSearch = (bool)$this->indexerConfig['enableMetaphoneSearch'];
201 $this->initializeExternalParsers();
202 // If "_sections" is set, this value overrides any existing value.
203 if ($searchData['_sections']) {
204 $searchData['sections'] = $searchData['_sections'];
205 }
206 // If "_sections" is set, this value overrides any existing value.
207 if ($searchData['_freeIndexUid'] !== '' && $searchData['_freeIndexUid'] !== '_') {
208 $searchData['freeIndexUid'] = $searchData['_freeIndexUid'];
209 }
210 $searchData['numberOfResults'] = $this->getNumberOfResults($searchData['numberOfResults']);
211 // This gets the search-words into the $searchWordArray
212 $this->setSword($searchData['sword']);
213 // Add previous search words to current
214 if ($searchData['sword_prev_include'] && $searchData['sword_prev']) {
215 $this->setSword(trim($searchData['sword_prev']) . ' ' . $this->getSword());
216 }
217 $this->searchWords = $this->getSearchWords($searchData['defaultOperand']);
218 // This is the id of the site root.
219 // This value may be a commalist of integer (prepared for this)
220 $this->searchRootPageIdList = (int)$GLOBALS['TSFE']->config['rootLine'][0]['uid'];
221 // Setting the list of root PIDs for the search. Notice, these page IDs MUST
222 // have a TypoScript template with root flag on them! Basically this list is used
223 // to select on the "rl0" field and page ids are registered as "rl0" only if
224 // a TypoScript template record with root flag is there.
225 // This happens AFTER the use of $this->searchRootPageIdList above because
226 // the above will then fetch the menu for the CURRENT site - regardless
227 // of this kind of searching here. Thus a general search will lookup in
228 // the WHOLE database while a specific section search will take the current sections.
229 if ($this->settings['rootPidList']) {
230 $this->searchRootPageIdList = implode(',', GeneralUtility::intExplode(',', $this->settings['rootPidList']));
231 }
232 $this->searchRepository = GeneralUtility::makeInstance(\TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository::class);
233 $this->searchRepository->initialize($this->settings, $searchData, $this->externalParsers, $this->searchRootPageIdList);
234 $this->searchData = $searchData;
235 // Calling hook for modification of initialized content
236 if ($hookObj = $this->hookRequest('initialize_postProc')) {
237 $hookObj->initialize_postProc();
238 }
239 return $searchData;
240 }
241
242 /**
243 * Performs the search, the display and writing stats
244 *
245 * @param array $search the search parameters, an associative array
246 * @ignorevalidation $search
247 */
248 public function searchAction($search = [])
249 {
250 $searchData = $this->initialize($search);
251 // Find free index uid:
252 $freeIndexUid = $searchData['freeIndexUid'];
253 if ($freeIndexUid == -2) {
254 $freeIndexUid = $this->settings['defaultFreeIndexUidList'];
255 } elseif (!isset($searchData['freeIndexUid'])) {
256 // index configuration is disabled
257 $freeIndexUid = -1;
258 }
259 $indexCfgs = GeneralUtility::intExplode(',', $freeIndexUid);
260 $resultsets = [];
261 foreach ($indexCfgs as $freeIndexUid) {
262 // Get result rows
263 $tstamp1 = GeneralUtility::milliseconds();
264 if ($hookObj = $this->hookRequest('getResultRows')) {
265 $resultData = $hookObj->getResultRows($this->searchWords, $freeIndexUid);
266 } else {
267 $resultData = $this->searchRepository->doSearch($this->searchWords, $freeIndexUid);
268 }
269 // Display search results
270 $tstamp2 = GeneralUtility::milliseconds();
271 if ($hookObj = $this->hookRequest('getDisplayResults')) {
272 $resultsets[$freeIndexUid] = $hookObj->getDisplayResults($this->searchWords, $resultData, $freeIndexUid);
273 } else {
274 $resultsets[$freeIndexUid] = $this->getDisplayResults($this->searchWords, $resultData, $freeIndexUid);
275 }
276 $tstamp3 = GeneralUtility::milliseconds();
277 // Create header if we are searching more than one indexing configuration
278 if (count($indexCfgs) > 1) {
279 if ($freeIndexUid > 0) {
280 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
281 ->getQueryBuilderForTable('index_config');
282 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
283 $indexCfgRec = $queryBuilder
284 ->select('*')
285 ->from('index_config')
286 ->where(
287 $queryBuilder->expr()->eq(
288 'uid',
289 $queryBuilder->createNamedParameter($freeIndexUid, \PDO::PARAM_INT)
290 )
291 )
292 ->execute()
293 ->fetch();
294 $categoryTitle = $indexCfgRec['title'];
295 } else {
296 $categoryTitle = LocalizationUtility::translate('indexingConfigurationHeader.' . $freeIndexUid, 'IndexedSearch');
297 }
298 $resultsets[$freeIndexUid]['categoryTitle'] = $categoryTitle;
299 }
300 // Write search statistics
301 $this->writeSearchStat($searchData, $this->searchWords, $resultData['count'], [$tstamp1, $tstamp2, $tstamp3]);
302 }
303 $this->view->assign('resultsets', $resultsets);
304 $this->view->assign('searchParams', $searchData);
305 $this->view->assign('searchWords', $this->searchWords);
306 }
307
308 /****************************************
309 * functions to make the result rows and result sets
310 * ready for the output
311 ***************************************/
312 /**
313 * Compiles the HTML display of the incoming array of result rows.
314 *
315 * @param array $searchWords Search words array (for display of text describing what was searched for)
316 * @param array $resultData Array with result rows, count, first row.
317 * @param int $freeIndexUid Pointing to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
318 * @return array
319 */
320 protected function getDisplayResults($searchWords, $resultData, $freeIndexUid = -1)
321 {
322 $result = [
323 'count' => $resultData['count'],
324 'searchWords' => $searchWords
325 ];
326 // Perform display of result rows array
327 if ($resultData) {
328 // Set first selected row (for calculation of ranking later)
329 $this->firstRow = $resultData['firstRow'];
330 // Result display here
331 $result['rows'] = $this->compileResultRows($resultData['resultRows'], $freeIndexUid);
332 $result['affectedSections'] = $this->resultSections;
333 // Browsing box
334 if ($resultData['count']) {
335 // could we get this in the view?
336 if ($this->searchData['group'] === 'sections' && $freeIndexUid <= 0) {
337 $resultSectionsCount = count($this->resultSections);
338 $result['sectionText'] = sprintf(LocalizationUtility::translate('result.' . ($resultSectionsCount > 1 ? 'inNsections' : 'inNsection'), 'IndexedSearch'), $resultSectionsCount);
339 }
340 }
341 }
342 // Print a message telling which words in which sections we searched for
343 if (substr($this->searchData['sections'], 0, 2) === 'rl') {
344 $result['searchedInSectionInfo'] = LocalizationUtility::translate('result.inSection', 'IndexedSearch') . ' "' . $this->getPathFromPageId(substr($this->searchData['sections'], 4)) . '"';
345 }
346 return $result;
347 }
348
349 /**
350 * Takes the array with resultrows as input and returns the result-HTML-code
351 * Takes the "group" var into account: Makes a "section" or "flat" display.
352 *
353 * @param array $resultRows Result rows
354 * @param int $freeIndexUid Pointing to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
355 * @return string HTML
356 */
357 protected function compileResultRows($resultRows, $freeIndexUid = -1)
358 {
359 $finalResultRows = [];
360 // Transfer result rows to new variable,
361 // performing some mapping of sub-results etc.
362 $newResultRows = [];
363 foreach ($resultRows as $row) {
364 $id = md5($row['phash_grouping']);
365 if (is_array($newResultRows[$id])) {
366 // swapping:
367 if (!$newResultRows[$id]['show_resume'] && $row['show_resume']) {
368 // Remove old
369 $subrows = $newResultRows[$id]['_sub'];
370 unset($newResultRows[$id]['_sub']);
371 $subrows[] = $newResultRows[$id];
372 // Insert new:
373 $newResultRows[$id] = $row;
374 $newResultRows[$id]['_sub'] = $subrows;
375 } else {
376 $newResultRows[$id]['_sub'][] = $row;
377 }
378 } else {
379 $newResultRows[$id] = $row;
380 }
381 }
382 $resultRows = $newResultRows;
383 $this->resultSections = [];
384 if ($freeIndexUid <= 0 && $this->searchData['group'] === 'sections') {
385 $rl2flag = substr($this->searchData['sections'], 0, 2) === 'rl';
386 $sections = [];
387 foreach ($resultRows as $row) {
388 $id = $row['rl0'] . '-' . $row['rl1'] . ($rl2flag ? '-' . $row['rl2'] : '');
389 $sections[$id][] = $row;
390 }
391 $this->resultSections = [];
392 foreach ($sections as $id => $resultRows) {
393 $rlParts = explode('-', $id);
394 if ($rlParts[2]) {
395 $theId = $rlParts[2];
396 $theRLid = 'rl2_' . $rlParts[2];
397 } elseif ($rlParts[1]) {
398 $theId = $rlParts[1];
399 $theRLid = 'rl1_' . $rlParts[1];
400 } else {
401 $theId = $rlParts[0];
402 $theRLid = '0';
403 }
404 $sectionName = $this->getPathFromPageId($theId);
405 $sectionName = ltrim($sectionName, '/');
406 if (!trim($sectionName)) {
407 $sectionTitleLinked = LocalizationUtility::translate('result.unnamedSection', 'IndexedSearch') . ':';
408 } else {
409 $onclick = 'document.forms[\'tx_indexedsearch\'][\'tx_indexedsearch_pi2[search][_sections]\'].value=' . GeneralUtility::quoteJSvalue($theRLid) . ';document.forms[\'tx_indexedsearch\'].submit();return false;';
410 $sectionTitleLinked = '<a href="#" onclick="' . htmlspecialchars($onclick) . '">' . $sectionName . ':</a>';
411 }
412 $resultRowsCount = count($resultRows);
413 $this->resultSections[$id] = [$sectionName, $resultRowsCount];
414 // Add section header
415 $finalResultRows[] = [
416 'isSectionHeader' => true,
417 'numResultRows' => $resultRowsCount,
418 'sectionId' => $id,
419 'sectionTitle' => $sectionTitleLinked
420 ];
421 // Render result rows
422 foreach ($resultRows as $row) {
423 $finalResultRows[] = $this->compileSingleResultRow($row);
424 }
425 }
426 } else {
427 // flat mode or no sections at all
428 foreach ($resultRows as $row) {
429 $finalResultRows[] = $this->compileSingleResultRow($row);
430 }
431 }
432 return $finalResultRows;
433 }
434
435 /**
436 * This prints a single result row, including a recursive call for subrows.
437 *
438 * @param array $row Search result row
439 * @param int $headerOnly 1=Display only header (for sub-rows!), 2=nothing at all
440 * @return string HTML code
441 */
442 protected function compileSingleResultRow($row, $headerOnly = 0)
443 {
444 $specRowConf = $this->getSpecialConfigurationForResultRow($row);
445 $resultData = $row;
446 $resultData['headerOnly'] = $headerOnly;
447 $resultData['CSSsuffix'] = $specRowConf['CSSsuffix'] ? '-' . $specRowConf['CSSsuffix'] : '';
448 if ($this->multiplePagesType($row['item_type'])) {
449 $dat = unserialize($row['cHashParams']);
450 $pp = explode('-', $dat['key']);
451 if ($pp[0] != $pp[1]) {
452 $resultData['titleaddition'] = ', ' . LocalizationUtility::translate('result.page', 'IndexedSearch') . ' ' . $dat['key'];
453 } else {
454 $resultData['titleaddition'] = ', ' . LocalizationUtility::translate('result.pages', 'IndexedSearch') . ' ' . $pp[0];
455 }
456 }
457 $title = $resultData['item_title'] . $resultData['titleaddition'];
458 $title = $this->charsetConverter->crop('utf-8', $title, $this->settings['results.']['titleCropAfter'], $this->settings['results.']['titleCropSignifier']);
459 // If external media, link to the media-file instead.
460 if ($row['item_type']) {
461 if ($row['show_resume']) {
462 // Can link directly.
463 $targetAttribute = '';
464 if ($GLOBALS['TSFE']->config['config']['fileTarget']) {
465 $targetAttribute = ' target="' . htmlspecialchars($GLOBALS['TSFE']->config['config']['fileTarget']) . '"';
466 }
467 $title = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($title) . '</a>';
468 } else {
469 // Suspicious, so linking to page instead...
470 $copiedRow = $row;
471 unset($copiedRow['cHashParams']);
472 $title = $this->linkPage($row['page_id'], htmlspecialchars($title), $copiedRow);
473 }
474 } else {
475 // Else the page:
476 // Prepare search words for markup in content:
477 $markUpSwParams = [];
478 if ($this->settings['forwardSearchWordsInResultLink']['_typoScriptNodeValue']) {
479 if ($this->settings['forwardSearchWordsInResultLink']['no_cache']) {
480 $markUpSwParams = ['no_cache' => 1];
481 }
482 foreach ($this->searchWords as $d) {
483 $markUpSwParams['sword_list'][] = $d['sword'];
484 }
485 }
486 $title = $this->linkPage($row['data_page_id'], htmlspecialchars($title), $row, $markUpSwParams);
487 }
488 $resultData['title'] = $title;
489 $resultData['icon'] = $this->makeItemTypeIcon($row['item_type'], '', $specRowConf);
490 $resultData['rating'] = $this->makeRating($row);
491 $resultData['description'] = $this->makeDescription(
492 $row,
493 (bool)!($this->searchData['extResume'] && !$headerOnly),
494 $this->settings['results.']['summaryCropAfter']
495 );
496 $resultData['language'] = $this->makeLanguageIndication($row);
497 $resultData['size'] = GeneralUtility::formatSize($row['item_size']);
498 $resultData['created'] = $row['item_crdate'];
499 $resultData['modified'] = $row['item_mtime'];
500 $pI = parse_url($row['data_filename']);
501 if ($pI['scheme']) {
502 $targetAttribute = '';
503 if ($GLOBALS['TSFE']->config['config']['fileTarget']) {
504 $targetAttribute = ' target="' . htmlspecialchars($GLOBALS['TSFE']->config['config']['fileTarget']) . '"';
505 }
506 $resultData['path'] = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($row['data_filename']) . '</a>';
507 } else {
508 $pathId = $row['data_page_id'] ?: $row['page_id'];
509 $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
510 $pathStr = $this->getPathFromPageId($pathId, $pathMP);
511 $resultData['path'] = $this->linkPage($pathId, $pathStr, [
512 'cHashParams' => $row['cHashParams'],
513 'data_page_type' => $row['data_page_type'],
514 'data_page_mp' => $pathMP,
515 'sys_language_uid' => $row['sys_language_uid']
516 ]);
517 // check if the access is restricted
518 if (is_array($this->requiredFrontendUsergroups[$pathId]) && !empty($this->requiredFrontendUsergroups[$pathId])) {
519 $resultData['access'] = '<img src="' . \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::siteRelPath('indexed_search')
520 . 'Resources/Public/Icons/FileTypes/locked.gif" width="12" height="15" vspace="5" title="'
521 . sprintf(LocalizationUtility::translate('result.memberGroups', 'IndexedSearch'), implode(',', array_unique($this->requiredFrontendUsergroups[$pathId])))
522 . '" alt="" />';
523 }
524 }
525 // If there are subrows (eg. subpages in a PDF-file or if a duplicate page
526 // is selected due to user-login (phash_grouping))
527 if (is_array($row['_sub'])) {
528 $resultData['subresults'] = [];
529 if ($this->multiplePagesType($row['item_type'])) {
530 $resultData['subresults']['header'] = LocalizationUtility::translate('result.otherMatching', 'IndexedSearch');
531 foreach ($row['_sub'] as $subRow) {
532 $resultData['subresults']['items'][] = $this->compileSingleResultRow($subRow, 1);
533 }
534 } else {
535 $resultData['subresults']['header'] = LocalizationUtility::translate('result.otherMatching', 'IndexedSearch');
536 $resultData['subresults']['info'] = LocalizationUtility::translate('result.otherPageAsWell', 'IndexedSearch');
537 }
538 }
539 return $resultData;
540 }
541
542 /**
543 * Returns configuration from TypoScript for result row based
544 * on ID / location in page tree!
545 *
546 * @param array $row Result row
547 * @return array Configuration array
548 */
549 protected function getSpecialConfigurationForResultRow($row)
550 {
551 $pathId = $row['data_page_id'] ?: $row['page_id'];
552 $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
553 $rl = $GLOBALS['TSFE']->sys_page->getRootLine($pathId, $pathMP);
554 $specConf = $this->settings['specialConfiguration']['0'];
555 if (is_array($rl)) {
556 foreach ($rl as $dat) {
557 if (is_array($this->settings['specialConfiguration'][$dat['uid']])) {
558 $specConf = $this->settings['specialConfiguration'][$dat['uid']];
559 $specConf['_pid'] = $dat['uid'];
560 break;
561 }
562 }
563 }
564 return $specConf;
565 }
566
567 /**
568 * Return the rating-HTML code for the result row. This makes use of the $this->firstRow
569 *
570 * @param array $row Result row array
571 * @return string String showing ranking value
572 * @todo can this be a ViewHelper?
573 */
574 protected function makeRating($row)
575 {
576 switch ((string)$this->searchData['sortOrder']) {
577 case 'rank_count':
578 return $row['order_val'] . ' ' . LocalizationUtility::translate('result.ratingMatches', 'IndexedSearch');
579 break;
580 case 'rank_first':
581 return ceil(MathUtility::forceIntegerInRange((255 - $row['order_val']), 1, 255) / 255 * 100) . '%';
582 break;
583 case 'rank_flag':
584 if ($this->firstRow['order_val2']) {
585 // (3 MSB bit, 224 is highest value of order_val1 currently)
586 $base = $row['order_val1'] * 256;
587 // 15-3 MSB = 12
588 $freqNumber = $row['order_val2'] / $this->firstRow['order_val2'] * pow(2, 12);
589 $total = MathUtility::forceIntegerInRange($base + $freqNumber, 0, 32767);
590 return ceil(log($total) / log(32767) * 100) . '%';
591 }
592 break;
593 case 'rank_freq':
594 $max = 10000;
595 $total = MathUtility::forceIntegerInRange($row['order_val'], 0, $max);
596 return ceil(log($total) / log($max) * 100) . '%';
597 break;
598 case 'crdate':
599 return $GLOBALS['TSFE']->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_crdate'], 0);
600 break;
601 case 'mtime':
602 return $GLOBALS['TSFE']->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_mtime'], 0);
603 break;
604 default:
605 return ' ';
606 }
607 }
608
609 /**
610 * Returns the HTML code for language indication.
611 *
612 * @param array $row Result row
613 * @return string HTML code for result row.
614 */
615 protected function makeLanguageIndication($row)
616 {
617 $output = '&nbsp;';
618 // If search result is a TYPO3 page:
619 if ((string)$row['item_type'] === '0') {
620 // If TypoScript is used to render the flag:
621 if (is_array($this->settings['flagRendering'])) {
622 /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
623 $cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
624 $cObj->setCurrentVal($row['sys_language_uid']);
625 $typoScriptArray = $this->typoScriptService->convertPlainArrayToTypoScriptArray($this->settings['flagRendering']);
626 $output = $cObj->cObjGetSingle($this->settings['flagRendering']['_typoScriptNodeValue'], $typoScriptArray);
627 }
628 }
629 return $output;
630 }
631
632 /**
633 * Return icon for file extension
634 *
635 * @param string $imageType File extension / item type
636 * @param string $alt Title attribute value in icon.
637 * @param array $specRowConf TypoScript configuration specifically for search result.
638 * @return string <img> tag for icon
639 */
640 public function makeItemTypeIcon($imageType, $alt, $specRowConf)
641 {
642 // Build compound key if item type is 0, iconRendering is not used
643 // and specialConfiguration.[pid].pageIcon was set in TS
644 if ($imageType === '0' && $specRowConf['_pid'] && is_array($specRowConf['pageIcon']) && !is_array($this->settings['iconRendering'])) {
645 $imageType .= ':' . $specRowConf['_pid'];
646 }
647 if (!isset($this->iconFileNameCache[$imageType])) {
648 $this->iconFileNameCache[$imageType] = '';
649 // If TypoScript is used to render the icon:
650 if (is_array($this->settings['iconRendering'])) {
651 /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
652 $cObj = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
653 $cObj->setCurrentVal($imageType);
654 $typoScriptArray = $this->typoScriptService->convertPlainArrayToTypoScriptArray($this->settings['iconRendering']);
655 $this->iconFileNameCache[$imageType] = $cObj->cObjGetSingle($this->settings['iconRendering']['_typoScriptNodeValue'], $typoScriptArray);
656 } else {
657 // Default creation / finding of icon:
658 $icon = '';
659 if ($imageType === '0' || substr($imageType, 0, 2) === '0:') {
660 if (is_array($specRowConf['pageIcon'])) {
661 $this->iconFileNameCache[$imageType] = $GLOBALS['TSFE']->cObj->cObjGetSingle('IMAGE', $specRowConf['pageIcon']);
662 } else {
663 $icon = 'EXT:indexed_search/Resources/Public/Icons/FileTypes/pages.gif';
664 }
665 } elseif ($this->externalParsers[$imageType]) {
666 $icon = $this->externalParsers[$imageType]->getIcon($imageType);
667 }
668 if ($icon) {
669 $fullPath = GeneralUtility::getFileAbsFileName($icon);
670 if ($fullPath) {
671 $info = @getimagesize($fullPath);
672 $iconPath = \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix($fullPath);
673 $this->iconFileNameCache[$imageType] = is_array($info) ? '<img src="' . $iconPath . '" ' . $info[3] . ' title="' . htmlspecialchars($alt) . '" alt="" />' : '';
674 }
675 }
676 }
677 }
678 return $this->iconFileNameCache[$imageType];
679 }
680
681 /**
682 * Returns the resume for the search-result.
683 *
684 * @param array $row Search result row
685 * @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.
686 * @param int $length String length
687 * @return string HTML string
688 * @todo overwork this
689 */
690 protected function makeDescription($row, $noMarkup = false, $length = 180)
691 {
692 if ($row['show_resume']) {
693 if (!$noMarkup) {
694 $markedSW = '';
695 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_fulltext');
696 $ftdrow = $queryBuilder
697 ->select('*')
698 ->from('index_fulltext')
699 ->where(
700 $queryBuilder->expr()->eq(
701 'phash',
702 $queryBuilder->createNamedParameter($row['phash'], \PDO::PARAM_INT)
703 )
704 )
705 ->execute()
706 ->fetch();
707 if ($ftdrow !== false) {
708 // Cut HTTP references after some length
709 $content = preg_replace('/(http:\\/\\/[^ ]{' . $this->settings['results.']['hrefInSummaryCropAfter'] . '})([^ ]+)/i', '$1...', $ftdrow['fulltextdata']);
710 $markedSW = $this->markupSWpartsOfString($content);
711 }
712 }
713 if (!trim($markedSW)) {
714 $outputStr = $this->charsetConverter->crop('utf-8', $row['item_description'], $length, $this->settings['results.']['summaryCropSignifier']);
715 $outputStr = htmlspecialchars($outputStr);
716 }
717 $output = $outputStr ?: $markedSW;
718 } else {
719 $output = '<span class="noResume">' . LocalizationUtility::translate('result.noResume', 'IndexedSearch') . '</span>';
720 }
721 return $output;
722 }
723
724 /**
725 * Marks up the search words from $this->searchWords in the $str with a color.
726 *
727 * @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.
728 * @return string Processed content
729 */
730 protected function markupSWpartsOfString($str)
731 {
732 $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
733 // Init:
734 $str = str_replace('&nbsp;', ' ', $htmlParser->bidir_htmlspecialchars($str, -1));
735 $str = preg_replace('/\\s\\s+/', ' ', $str);
736 $swForReg = [];
737 // Prepare search words for regex:
738 foreach ($this->searchWords as $d) {
739 $swForReg[] = preg_quote($d['sword'], '/');
740 }
741 $regExString = '(' . implode('|', $swForReg) . ')';
742 // Split and combine:
743 $parts = preg_split('/' . $regExString . '/i', ' ' . $str . ' ', 20000, PREG_SPLIT_DELIM_CAPTURE);
744 // Constants:
745 $summaryMax = $this->settings['results.']['markupSW_summaryMax'];
746 $postPreLgd = $this->settings['results.']['markupSW_postPreLgd'];
747 $postPreLgd_offset = $this->settings['results.']['markupSW_postPreLgd_offset'];
748 $divider = $this->settings['results.']['markupSW_divider'];
749 $occurencies = (count($parts) - 1) / 2;
750 if ($occurencies) {
751 $postPreLgd = MathUtility::forceIntegerInRange($summaryMax / $occurencies, $postPreLgd, $summaryMax / 2);
752 }
753 // Variable:
754 $summaryLgd = 0;
755 $output = [];
756 // Shorten in-between strings:
757 foreach ($parts as $k => $strP) {
758 if ($k % 2 == 0) {
759 // Find length of the summary part:
760 $strLen = mb_strlen($parts[$k], 'utf-8');
761 $output[$k] = $parts[$k];
762 // Possibly shorten string:
763 if (!$k) {
764 // First entry at all (only cropped on the frontside)
765 if ($strLen > $postPreLgd) {
766 $output[$k] = $divider . preg_replace('/^[^[:space:]]+[[:space:]]/', '', $this->charsetConverter->crop('utf-8', $parts[$k], -($postPreLgd - $postPreLgd_offset)));
767 }
768 } elseif ($summaryLgd > $summaryMax || !isset($parts[$k + 1])) {
769 // In case summary length is exceed OR if there are no more entries at all:
770 if ($strLen > $postPreLgd) {
771 $output[$k] = preg_replace('/[[:space:]][^[:space:]]+$/', '', $this->charsetConverter->crop('utf-8', $parts[$k], ($postPreLgd - $postPreLgd_offset))) . $divider;
772 }
773 } else {
774 if ($strLen > $postPreLgd * 2) {
775 $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)));
776 }
777 }
778 $summaryLgd += mb_strlen($output[$k], 'utf-8');
779 // Protect output:
780 $output[$k] = htmlspecialchars($output[$k]);
781 // If summary lgd is exceed, break the process:
782 if ($summaryLgd > $summaryMax) {
783 break;
784 }
785 } else {
786 $summaryLgd += mb_strlen($strP, 'utf-8');
787 $output[$k] = '<strong class="tx-indexedsearch-redMarkup">' . htmlspecialchars($parts[$k]) . '</strong>';
788 }
789 }
790 // Return result:
791 return implode('', $output);
792 }
793
794 /**
795 * Write statistics information to database for the search operation
796 *
797 * @param array $searchParams search params
798 * @param array $searchWords Search Word array
799 * @param int $count Number of hits
800 * @param int $pt Milliseconds the search took
801 */
802 protected function writeSearchStat($searchParams, $searchWords, $count, $pt)
803 {
804 $insertFields = [
805 'searchstring' => $this->getSword(),
806 'searchoptions' => serialize([$searchParams, $searchWords, $pt]),
807 'feuser_id' => (int)$GLOBALS['TSFE']->fe_user->user['uid'],
808 // cookie as set or retrieved. If people has cookies disabled this will vary all the time
809 'cookie' => $GLOBALS['TSFE']->fe_user->id,
810 // Remote IP address
811 'IP' => GeneralUtility::getIndpEnv('REMOTE_ADDR'),
812 // Number of hits on the search
813 'hits' => (int)$count,
814 // Time stamp
815 'tstamp' => $GLOBALS['EXEC_TIME']
816 ];
817 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('index_search_stat');
818 $connection->insert(
819 'index_stat_search',
820 $insertFields,
821 ['searchoptions' => Connection::PARAM_LOB]
822 );
823 $newId = $connection->lastInsertId('index_stat_search');
824 if ($newId) {
825 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('index_stat_word');
826 foreach ($searchWords as $val) {
827 $insertFields = [
828 'word' => $val['sword'],
829 'index_stat_search_id' => $newId,
830 // Time stamp
831 'tstamp' => $GLOBALS['EXEC_TIME'],
832 // search page id for indexed search stats
833 'pageid' => $GLOBALS['TSFE']->id
834 ];
835 $connection->insert('index_stat_word', $insertFields);
836 }
837 }
838 }
839
840 /**
841 * Splits the search word input into an array where each word is represented by an array with key "sword"
842 * holding the search word and key "oper" holding the SQL operator (eg. AND, OR)
843 *
844 * Only words with 2 or more characters are accepted
845 * Max 200 chars total
846 * Space is used to split words, "" can be used search for a whole string
847 * AND, OR and NOT are prefix words, overruling the default operator
848 * +/|/- equals AND, OR and NOT as operators.
849 * All search words are converted to lowercase.
850 *
851 * $defOp is the default operator. 1=OR, 0=AND
852 *
853 * @param bool $defaultOperator If TRUE, the default operator will be OR, not AND
854 * @return array Search words if any found
855 */
856 protected function getSearchWords($defaultOperator)
857 {
858 // Shorten search-word string to max 200 bytes (does NOT take multibyte charsets into account - but never mind,
859 // shortening the string here is only a run-away feature!)
860 $searchWords = substr($this->getSword(), 0, 200);
861 // Convert to UTF-8 + conv. entities (was also converted during indexing!)
862 $searchWords = $this->charsetConverter->conv($searchWords, $GLOBALS['TSFE']->metaCharset, 'utf-8');
863 $searchWords = $this->charsetConverter->entities_to_utf8($searchWords);
864 $sWordArray = false;
865 if ($hookObj = $this->hookRequest('getSearchWords')) {
866 $sWordArray = $hookObj->getSearchWords_splitSWords($searchWords, $defaultOperator);
867 } else {
868 // sentence
869 if ($this->searchData['searchType'] == 20) {
870 $sWordArray = [
871 [
872 'sword' => trim($searchWords),
873 'oper' => 'AND'
874 ]
875 ];
876 } else {
877 // case-sensitive. Defines the words, which will be
878 // operators between words
879 $operatorTranslateTable = [
880 ['+', 'AND'],
881 ['|', 'OR'],
882 ['-', 'AND NOT'],
883 // Add operators for various languages
884 // Converts the operators to lowercase
885 [mb_strtolower(LocalizationUtility::translate('localizedOperandAnd', 'IndexedSearch'), 'utf-8'), 'AND'],
886 [mb_strtolower(LocalizationUtility::translate('localizedOperandOr', 'IndexedSearch'), 'utf-8'), 'OR'],
887 [mb_strtolower(LocalizationUtility::translate('localizedOperandNot', 'IndexedSearch'), 'utf-8'), 'AND NOT']
888 ];
889 $swordArray = \TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::getExplodedSearchString($searchWords, $defaultOperator == 1 ? 'OR' : 'AND', $operatorTranslateTable);
890 if (is_array($swordArray)) {
891 $sWordArray = $this->procSearchWordsByLexer($swordArray);
892 }
893 }
894 }
895 return $sWordArray;
896 }
897
898 /**
899 * Post-process the search word array so it will match the words that was indexed (including case-folding if any)
900 * If any words are splitted into multiple words (eg. CJK will be!) the operator of the main word will remain.
901 *
902 * @param array $searchWords Search word array
903 * @return array Search word array, processed through lexer
904 */
905 protected function procSearchWordsByLexer($searchWords)
906 {
907 $newSearchWords = [];
908 // Init lexer (used to post-processing of search words)
909 $lexerObjRef = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['lexer'] ?: \TYPO3\CMS\IndexedSearch\Lexer::class;
910 $this->lexerObj = GeneralUtility::getUserObj($lexerObjRef);
911 // Traverse the search word array
912 foreach ($searchWords as $wordDef) {
913 // No space in word (otherwise it might be a sentense in quotes like "there is").
914 if (strpos($wordDef['sword'], ' ') === false) {
915 // Split the search word by lexer:
916 $res = $this->lexerObj->split2Words($wordDef['sword']);
917 // Traverse lexer result and add all words again:
918 foreach ($res as $word) {
919 $newSearchWords[] = [
920 'sword' => $word,
921 'oper' => $wordDef['oper']
922 ];
923 }
924 } else {
925 $newSearchWords[] = $wordDef;
926 }
927 }
928 return $newSearchWords;
929 }
930
931 /**
932 * Sort options about the search form
933 *
934 * @param array $search The search data / params
935 * @ignorevalidation $search
936 */
937 public function formAction($search = [])
938 {
939 $searchData = $this->initialize($search);
940 // Adding search field value
941 $this->view->assign('sword', $this->getSword());
942 // Extended search
943 if ($search['extendedSearch']) {
944 // "Search for"
945 $allSearchTypes = $this->getAllAvailableSearchTypeOptions();
946 $this->view->assign('allSearchTypes', $allSearchTypes);
947 $allDefaultOperands = $this->getAllAvailableOperandsOptions();
948 $this->view->assign('allDefaultOperands', $allDefaultOperands);
949 $showTypeSearch = !empty($allSearchTypes) || !empty($allDefaultOperands);
950 $this->view->assign('showTypeSearch', $showTypeSearch);
951 // "Search in"
952 $allMediaTypes = $this->getAllAvailableMediaTypesOptions();
953 $this->view->assign('allMediaTypes', $allMediaTypes);
954 $allLanguageUids = $this->getAllAvailableLanguageOptions();
955 $this->view->assign('allLanguageUids', $allLanguageUids);
956 $showMediaAndLanguageSearch = !empty($allMediaTypes) || !empty($allLanguageUids);
957 $this->view->assign('showMediaAndLanguageSearch', $showMediaAndLanguageSearch);
958 // Sections
959 $allSections = $this->getAllAvailableSectionsOptions();
960 $this->view->assign('allSections', $allSections);
961 // Free Indexing Configurations
962 $allIndexConfigurations = $this->getAllAvailableIndexConfigurationsOptions();
963 $this->view->assign('allIndexConfigurations', $allIndexConfigurations);
964 // Sorting
965 $allSortOrders = $this->getAllAvailableSortOrderOptions();
966 $this->view->assign('allSortOrders', $allSortOrders);
967 $allSortDescendings = $this->getAllAvailableSortDescendingOptions();
968 $this->view->assign('allSortDescendings', $allSortDescendings);
969 $showSortOrders = !empty($allSortOrders) || !empty($allSortDescendings);
970 $this->view->assign('showSortOrders', $showSortOrders);
971 // Limits
972 $allNumberOfResults = $this->getAllAvailableNumberOfResultsOptions();
973 $this->view->assign('allNumberOfResults', $allNumberOfResults);
974 $allGroups = $this->getAllAvailableGroupOptions();
975 $this->view->assign('allGroups', $allGroups);
976 }
977 $this->view->assign('searchParams', $searchData);
978 }
979
980 /**
981 * TypoScript was not loaded
982 */
983 public function noTypoScriptAction()
984 {
985 }
986
987 /****************************************
988 * building together the available options for every dropdown
989 ***************************************/
990 /**
991 * get the values for the "type" selector
992 *
993 * @return array Associative array with options
994 */
995 protected function getAllAvailableSearchTypeOptions()
996 {
997 $allOptions = [];
998 $types = [0, 1, 2, 3, 10, 20];
999 $blindSettings = $this->settings['blind'];
1000 if (!$blindSettings['searchType']) {
1001 foreach ($types as $typeNum) {
1002 $allOptions[$typeNum] = LocalizationUtility::translate('searchTypes.' . $typeNum, 'IndexedSearch');
1003 }
1004 }
1005 // Remove this option if metaphone search is disabled)
1006 if (!$this->enableMetaphoneSearch) {
1007 unset($allOptions[10]);
1008 }
1009 // disable single entries by TypoScript
1010 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['searchType']);
1011 return $allOptions;
1012 }
1013
1014 /**
1015 * get the values for the "defaultOperand" selector
1016 *
1017 * @return array Associative array with options
1018 */
1019 protected function getAllAvailableOperandsOptions()
1020 {
1021 $allOptions = [];
1022 $blindSettings = $this->settings['blind'];
1023 if (!$blindSettings['defaultOperand']) {
1024 $allOptions = [
1025 0 => LocalizationUtility::translate('defaultOperands.0', 'IndexedSearch'),
1026 1 => LocalizationUtility::translate('defaultOperands.1', 'IndexedSearch')
1027 ];
1028 }
1029 // disable single entries by TypoScript
1030 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['defaultOperand']);
1031 return $allOptions;
1032 }
1033
1034 /**
1035 * get the values for the "media type" selector
1036 *
1037 * @return array Associative array with options
1038 */
1039 protected function getAllAvailableMediaTypesOptions()
1040 {
1041 $allOptions = [];
1042 $mediaTypes = [-1, 0, -2];
1043 $blindSettings = $this->settings['blind'];
1044 if (!$blindSettings['mediaType']) {
1045 foreach ($mediaTypes as $mediaType) {
1046 $allOptions[$mediaType] = LocalizationUtility::translate('mediaTypes.' . $mediaType, 'IndexedSearch');
1047 }
1048 // Add media to search in:
1049 $additionalMedia = trim($this->settings['mediaList']);
1050 if ($additionalMedia !== '') {
1051 $additionalMedia = GeneralUtility::trimExplode(',', $additionalMedia, true);
1052 } else {
1053 $additionalMedia = [];
1054 }
1055 foreach ($this->externalParsers as $extension => $obj) {
1056 // Skip unwanted extensions
1057 if (!empty($additionalMedia) && !in_array($extension, $additionalMedia)) {
1058 continue;
1059 }
1060 if ($name = $obj->searchTypeMediaTitle($extension)) {
1061 $translatedName = LocalizationUtility::translate('mediaTypes.' . $extension, 'IndexedSearch');
1062 $allOptions[$extension] = $translatedName ?: $name;
1063 }
1064 }
1065 }
1066 // disable single entries by TypoScript
1067 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['mediaType']);
1068 return $allOptions;
1069 }
1070
1071 /**
1072 * get the values for the "language" selector
1073 *
1074 * @return array Associative array with options
1075 */
1076 protected function getAllAvailableLanguageOptions()
1077 {
1078 $allOptions = [
1079 '-1' => LocalizationUtility::translate('languageUids.-1', 'IndexedSearch'),
1080 '0' => LocalizationUtility::translate('languageUids.0', 'IndexedSearch')
1081 ];
1082 $blindSettings = $this->settings['blind'];
1083 if (!$blindSettings['languageUid']) {
1084 // Add search languages
1085 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1086 ->getQueryBuilderForTable('sys_language');
1087 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1088 $result = $queryBuilder
1089 ->select('uid', 'title')
1090 ->from('sys_language')
1091 ->execute();
1092
1093 while ($lang = $result->fetch()) {
1094 $allOptions[$lang['uid']] = $lang['title'];
1095 }
1096 // disable single entries by TypoScript
1097 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['languageUid']);
1098 } else {
1099 $allOptions = [];
1100 }
1101 return $allOptions;
1102 }
1103
1104 /**
1105 * get the values for the "section" selector
1106 * Here values like "rl1_" and "rl2_" + a rootlevel 1/2 id can be added
1107 * to perform searches in rootlevel 1+2 specifically. The id-values can even
1108 * be commaseparated. Eg. "rl1_1,2" would search for stuff inside pages on
1109 * menu-level 1 which has the uid's 1 and 2.
1110 *
1111 * @return array Associative array with options
1112 */
1113 protected function getAllAvailableSectionsOptions()
1114 {
1115 $allOptions = [];
1116 $sections = [0, -1, -2, -3];
1117 $blindSettings = $this->settings['blind'];
1118 if (!$blindSettings['sections']) {
1119 foreach ($sections as $section) {
1120 $allOptions[$section] = LocalizationUtility::translate('sections.' . $section, 'IndexedSearch');
1121 }
1122 }
1123 // Creating levels for section menu:
1124 // This selects the first and secondary menus for the "sections" selector - so we can search in sections and sub sections.
1125 if ($this->settings['displayLevel1Sections']) {
1126 $firstLevelMenu = $this->getMenuOfPages($this->searchRootPageIdList);
1127 $labelLevel1 = LocalizationUtility::translate('sections.rootLevel1', 'IndexedSearch');
1128 $labelLevel2 = LocalizationUtility::translate('sections.rootLevel2', 'IndexedSearch');
1129 foreach ($firstLevelMenu as $firstLevelKey => $menuItem) {
1130 if (!$menuItem['nav_hide']) {
1131 $allOptions['rl1_' . $menuItem['uid']] = trim($labelLevel1 . ' ' . $menuItem['title']);
1132 if ($this->settings['displayLevel2Sections']) {
1133 $secondLevelMenu = $this->getMenuOfPages($menuItem['uid']);
1134 foreach ($secondLevelMenu as $secondLevelKey => $menuItemLevel2) {
1135 if (!$menuItemLevel2['nav_hide']) {
1136 $allOptions['rl2_' . $menuItemLevel2['uid']] = trim($labelLevel2 . ' ' . $menuItemLevel2['title']);
1137 } else {
1138 unset($secondLevelMenu[$secondLevelKey]);
1139 }
1140 }
1141 $allOptions['rl2_' . implode(',', array_keys($secondLevelMenu))] = LocalizationUtility::translate('sections.rootLevel2All', 'IndexedSearch');
1142 }
1143 } else {
1144 unset($firstLevelMenu[$firstLevelKey]);
1145 }
1146 }
1147 $allOptions['rl1_' . implode(',', array_keys($firstLevelMenu))] = LocalizationUtility::translate('sections.rootLevel1All', 'IndexedSearch');
1148 }
1149 // disable single entries by TypoScript
1150 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['sections']);
1151 return $allOptions;
1152 }
1153
1154 /**
1155 * get the values for the "freeIndexUid" selector
1156 *
1157 * @return array Associative array with options
1158 */
1159 protected function getAllAvailableIndexConfigurationsOptions()
1160 {
1161 $allOptions = [
1162 '-1' => LocalizationUtility::translate('indexingConfigurations.-1', 'IndexedSearch'),
1163 '-2' => LocalizationUtility::translate('indexingConfigurations.-2', 'IndexedSearch'),
1164 '0' => LocalizationUtility::translate('indexingConfigurations.0', 'IndexedSearch')
1165 ];
1166 $blindSettings = $this->settings['blind'];
1167 if (!$blindSettings['indexingConfigurations']) {
1168 // add an additional index configuration
1169 if ($this->settings['defaultFreeIndexUidList']) {
1170 $uidList = GeneralUtility::intExplode(',', $this->settings['defaultFreeIndexUidList']);
1171 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1172 ->getQueryBuilderForTable('index_config');
1173 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1174 $result = $queryBuilder
1175 ->select('uid', 'title')
1176 ->from('index_config')
1177 ->where(
1178 $queryBuilder->expr()->in(
1179 'uid',
1180 $queryBuilder->createNamedParameter($uidList, Connection::PARAM_INT_ARRAY)
1181 )
1182 )
1183 ->execute();
1184
1185 while ($row = $result->fetch()) {
1186 $allOptions[$row['uid']]= $row['title'];
1187 }
1188 }
1189 // disable single entries by TypoScript
1190 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['indexingConfigurations']);
1191 } else {
1192 $allOptions = [];
1193 }
1194 return $allOptions;
1195 }
1196
1197 /**
1198 * get the values for the "section" selector
1199 * Here values like "rl1_" and "rl2_" + a rootlevel 1/2 id can be added
1200 * to perform searches in rootlevel 1+2 specifically. The id-values can even
1201 * be commaseparated. Eg. "rl1_1,2" would search for stuff inside pages on
1202 * menu-level 1 which has the uid's 1 and 2.
1203 *
1204 * @return array Associative array with options
1205 */
1206 protected function getAllAvailableSortOrderOptions()
1207 {
1208 $allOptions = [];
1209 $sortOrders = ['rank_flag', 'rank_freq', 'rank_first', 'rank_count', 'mtime', 'title', 'crdate'];
1210 $blindSettings = $this->settings['blind'];
1211 if (!$blindSettings['sortOrder']) {
1212 foreach ($sortOrders as $order) {
1213 $allOptions[$order] = LocalizationUtility::translate('sortOrders.' . $order, 'IndexedSearch');
1214 }
1215 }
1216 // disable single entries by TypoScript
1217 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['sortOrder.']);
1218 return $allOptions;
1219 }
1220
1221 /**
1222 * get the values for the "group" selector
1223 *
1224 * @return array Associative array with options
1225 */
1226 protected function getAllAvailableGroupOptions()
1227 {
1228 $allOptions = [];
1229 $blindSettings = $this->settings['blind'];
1230 if (!$blindSettings['groupBy']) {
1231 $allOptions = [
1232 'sections' => LocalizationUtility::translate('groupBy.sections', 'IndexedSearch'),
1233 'flat' => LocalizationUtility::translate('groupBy.flat', 'IndexedSearch')
1234 ];
1235 }
1236 // disable single entries by TypoScript
1237 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['groupBy.']);
1238 return $allOptions;
1239 }
1240
1241 /**
1242 * get the values for the "sortDescending" selector
1243 *
1244 * @return array Associative array with options
1245 */
1246 protected function getAllAvailableSortDescendingOptions()
1247 {
1248 $allOptions = [];
1249 $blindSettings = $this->settings['blind'];
1250 if (!$blindSettings['descending']) {
1251 $allOptions = [
1252 0 => LocalizationUtility::translate('sortOrders.descending', 'IndexedSearch'),
1253 1 => LocalizationUtility::translate('sortOrders.ascending', 'IndexedSearch')
1254 ];
1255 }
1256 // disable single entries by TypoScript
1257 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['descending.']);
1258 return $allOptions;
1259 }
1260
1261 /**
1262 * get the values for the "results" selector
1263 *
1264 * @return array Associative array with options
1265 */
1266 protected function getAllAvailableNumberOfResultsOptions()
1267 {
1268 $allOptions = [];
1269 if (count($this->availableResultsNumbers) > 1) {
1270 $allOptions = array_combine($this->availableResultsNumbers, $this->availableResultsNumbers);
1271 }
1272 // disable single entries by TypoScript
1273 $allOptions = $this->removeOptionsFromOptionList($allOptions, $this->settings['blind']['numberOfResults']);
1274 return $allOptions;
1275 }
1276
1277 /**
1278 * removes blinding entries from the option list of a selector
1279 *
1280 * @param array $allOptions associative array containing all options
1281 * @param array $blindOptions associative array containing the optionkey as they key and the value = 1 if it should be removed
1282 * @return array Options from $allOptions with some options removed
1283 */
1284 protected function removeOptionsFromOptionList($allOptions, $blindOptions)
1285 {
1286 if (is_array($blindOptions)) {
1287 foreach ($blindOptions as $key => $val) {
1288 if ($val == 1) {
1289 unset($allOptions[$key]);
1290 }
1291 }
1292 }
1293 return $allOptions;
1294 }
1295
1296 /**
1297 * Links the $linkText to page $pageUid
1298 *
1299 * @param int $pageUid Page id
1300 * @param string $linkText Title to link (must already be escaped for HTML output)
1301 * @param array $row Result row
1302 * @param array $markUpSwParams Additional parameters for marking up search words
1303 * @return string <A> tag wrapped title string.
1304 * @todo make use of the UriBuilder
1305 */
1306 protected function linkPage($pageUid, $linkText, $row = [], $markUpSwParams = [])
1307 {
1308 // Parameters for link
1309 $urlParameters = (array)unserialize($row['cHashParams']);
1310 // Add &type and &MP variable:
1311 if ($row['data_page_mp']) {
1312 $urlParameters['MP'] = $row['data_page_mp'];
1313 }
1314 $urlParameters['L'] = intval($row['sys_language_uid']);
1315 // markup-GET vars:
1316 $urlParameters = array_merge($urlParameters, $markUpSwParams);
1317 // This will make sure that the path is retrieved if it hasn't been
1318 // already. Used only for the sake of the domain_record thing.
1319 if (!is_array($this->domainRecords[$pageUid])) {
1320 $this->getPathFromPageId($pageUid);
1321 }
1322 $target = '';
1323 // If external domain, then link to that:
1324 if (!empty($this->domainRecords[$pageUid])) {
1325 $scheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://';
1326 $firstDomain = reset($this->domainRecords[$pageUid]);
1327 $additionalParams = '';
1328 if (is_array($urlParameters) && !empty($urlParameters)) {
1329 $additionalParams = GeneralUtility::implodeArrayForUrl('', $urlParameters);
1330 }
1331 $uri = $scheme . $firstDomain . '/index.php?id=' . $pageUid . $additionalParams;
1332 if ($target = $this->settings['detectDomainRecords.']['target']) {
1333 $target = ' target="' . $target . '"';
1334 }
1335 } else {
1336 $uriBuilder = $this->controllerContext->getUriBuilder();
1337 $uri = $uriBuilder->setTargetPageUid($pageUid)->setTargetPageType($row['data_page_type'])->setUseCacheHash(true)->setArguments($urlParameters)->build();
1338 }
1339 return '<a href="' . htmlspecialchars($uri) . '"' . $target . '>' . $linkText . '</a>';
1340 }
1341
1342 /**
1343 * Return the menu of pages used for the selector.
1344 *
1345 * @param int $pageUid Page ID for which to return menu
1346 * @return array Menu items (for making the section selector box)
1347 */
1348 protected function getMenuOfPages($pageUid)
1349 {
1350 if ($this->settings['displayLevelxAllTypes']) {
1351 $menu = [];
1352 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1353 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1354 $result = $queryBuilder
1355 ->select('uid', 'title')
1356 ->from('pages')
1357 ->where(
1358 $queryBuilder->expr()->eq(
1359 'pid',
1360 $queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)
1361 )
1362 )
1363 ->orderBy('sorting')
1364 ->execute();
1365
1366 while ($row = $result->fetch()) {
1367 $menu[$row['uid']] = $GLOBALS['TSFE']->sys_page->getPageOverlay($row);
1368 }
1369 } else {
1370 $menu = $GLOBALS['TSFE']->sys_page->getMenu($pageUid);
1371 }
1372 return $menu;
1373 }
1374
1375 /**
1376 * Returns the path to the page $id
1377 *
1378 * @param int $id Page ID
1379 * @param string $pathMP Content of the MP (mount point) variable
1380 * @return string Path (HTML-escaped)
1381 */
1382 protected function getPathFromPageId($id, $pathMP = '')
1383 {
1384 $identStr = $id . '|' . $pathMP;
1385 if (!isset($this->pathCache[$identStr])) {
1386 $this->requiredFrontendUsergroups[$id] = [];
1387 $this->domainRecords[$id] = [];
1388 $rl = $GLOBALS['TSFE']->sys_page->getRootLine($id, $pathMP);
1389 $path = '';
1390 $pageCount = count($rl);
1391 if (is_array($rl) && !empty($rl)) {
1392 $breadcrumbWrap = isset($this->settings['breadcrumbWrap']) ? $this->settings['breadcrumbWrap'] : '/';
1393 $breadcrumbWraps = $GLOBALS['TSFE']->tmpl->splitConfArray(['wrap' => $breadcrumbWrap], $pageCount);
1394 foreach ($rl as $k => $v) {
1395 // Check fe_user
1396 if ($v['fe_group'] && ($v['uid'] == $id || $v['extendToSubpages'])) {
1397 $this->requiredFrontendUsergroups[$id][] = $v['fe_group'];
1398 }
1399 // Check sys_domain
1400 if ($this->settings['detectDomainRcords']) {
1401 $domainName = $this->getFirstSysDomainRecordForPage($v['uid']);
1402 if ($domainName) {
1403 $this->domainRecords[$id][] = $domainName;
1404 // Set path accordingly
1405 $path = $domainName . $path;
1406 break;
1407 }
1408 }
1409 // Stop, if we find that the current id is the current root page.
1410 if ($v['uid'] == $GLOBALS['TSFE']->config['rootLine'][0]['uid']) {
1411 array_pop($breadcrumbWraps);
1412 break;
1413 }
1414 $path = $GLOBALS['TSFE']->cObj->wrap(htmlspecialchars($v['title']), array_pop($breadcrumbWraps)['wrap']) . $path;
1415 }
1416 }
1417 $this->pathCache[$identStr] = $path;
1418 }
1419 return $this->pathCache[$identStr];
1420 }
1421
1422 /**
1423 * Gets the first sys_domain record for the page, $id
1424 *
1425 * @param int $id Page id
1426 * @return string Domain name
1427 */
1428 protected function getFirstSysDomainRecordForPage($id)
1429 {
1430 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_domain');
1431 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1432 $row = $queryBuilder
1433 ->select('domainName')
1434 ->from('sys_domain')
1435 ->where(
1436 $queryBuilder->expr()->eq(
1437 'pid',
1438 $queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
1439 )
1440 )
1441 ->orderBy('sorting')
1442 ->setMaxResults(1)
1443 ->execute()
1444 ->fetch();
1445
1446 return rtrim($row['domainName'], '/');
1447 }
1448
1449 /**
1450 * simple function to initialize possible external parsers
1451 * feeds the $this->externalParsers array
1452 */
1453 protected function initializeExternalParsers()
1454 {
1455 // Initialize external document parsers for icon display and other soft operations
1456 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['external_parsers'])) {
1457 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['external_parsers'] as $extension => $_objRef) {
1458 $this->externalParsers[$extension] = GeneralUtility::getUserObj($_objRef);
1459 // Init parser and if it returns FALSE, unset its entry again
1460 if (!$this->externalParsers[$extension]->softInit($extension)) {
1461 unset($this->externalParsers[$extension]);
1462 }
1463 }
1464 }
1465 }
1466
1467 /**
1468 * Returns an object reference to the hook object if any
1469 *
1470 * @param string $functionName Name of the function you want to call / hook key
1471 * @return object|NULL Hook object, if any. Otherwise NULL.
1472 */
1473 protected function hookRequest($functionName)
1474 {
1475 // Hook: menuConfig_preProcessModMenu
1476 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
1477 $hookObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
1478 if (method_exists($hookObj, $functionName)) {
1479 $hookObj->pObj = $this;
1480 return $hookObj;
1481 }
1482 }
1483 return null;
1484 }
1485
1486 /**
1487 * Returns if an item type is a multipage item type
1488 *
1489 * @param string $item_type Item type
1490 * @return bool TRUE if multipage capable
1491 */
1492 protected function multiplePagesType($item_type)
1493 {
1494 return is_object($this->externalParsers[$item_type]) && $this->externalParsers[$item_type]->isMultiplePageExtension($item_type);
1495 }
1496
1497 /**
1498 * Load settings and apply stdWrap to them
1499 */
1500 protected function loadSettings()
1501 {
1502 if (!is_array($this->settings['results.'])) {
1503 $this->settings['results.'] = [];
1504 }
1505 $typoScriptArray = $this->typoScriptService->convertPlainArrayToTypoScriptArray($this->settings['results']);
1506
1507 $this->settings['results.']['summaryCropAfter'] = MathUtility::forceIntegerInRange(
1508 $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['summaryCropAfter'], $typoScriptArray['summaryCropAfter.']),
1509 10, 5000, 180
1510 );
1511 $this->settings['results.']['summaryCropSignifier'] = $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['summaryCropSignifier'], $typoScriptArray['summaryCropSignifier.']);
1512 $this->settings['results.']['titleCropAfter'] = MathUtility::forceIntegerInRange(
1513 $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['titleCropAfter'], $typoScriptArray['titleCropAfter.']),
1514 10, 500, 50
1515 );
1516 $this->settings['results.']['titleCropSignifier'] = $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['titleCropSignifier'], $typoScriptArray['titleCropSignifier.']);
1517 $this->settings['results.']['markupSW_summaryMax'] = MathUtility::forceIntegerInRange(
1518 $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['markupSW_summaryMax'], $typoScriptArray['markupSW_summaryMax.']),
1519 10, 5000, 300
1520 );
1521 $this->settings['results.']['markupSW_postPreLgd'] = MathUtility::forceIntegerInRange(
1522 $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['markupSW_postPreLgd'], $typoScriptArray['markupSW_postPreLgd.']),
1523 1, 500, 60
1524 );
1525 $this->settings['results.']['markupSW_postPreLgd_offset'] = MathUtility::forceIntegerInRange(
1526 $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['markupSW_postPreLgd_offset'], $typoScriptArray['markupSW_postPreLgd_offset.']),
1527 1, 50, 5
1528 );
1529 $this->settings['results.']['markupSW_divider'] = $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['markupSW_divider'], $typoScriptArray['markupSW_divider.']);
1530 $this->settings['results.']['hrefInSummaryCropAfter'] = MathUtility::forceIntegerInRange(
1531 $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['hrefInSummaryCropAfter'], $typoScriptArray['hrefInSummaryCropAfter.']),
1532 10, 400, 60
1533 );
1534 $this->settings['results.']['hrefInSummaryCropSignifier'] = $GLOBALS['TSFE']->cObj->stdWrap($typoScriptArray['hrefInSummaryCropSignifier'], $typoScriptArray['hrefInSummaryCropSignifier.']);
1535 }
1536
1537 /**
1538 * Returns number of results to display
1539 *
1540 * @param int $numberOfResults Requested number of results
1541 * @return int
1542 */
1543 protected function getNumberOfResults($numberOfResults)
1544 {
1545 $numberOfResults = intval($numberOfResults);
1546
1547 return (in_array($numberOfResults, $this->availableResultsNumbers)) ?
1548 $numberOfResults : $this->defaultResultNumber;
1549 }
1550
1551 /**
1552 * Set the search word
1553 * @param string $sword
1554 */
1555 public function setSword($sword)
1556 {
1557 $this->sword = $sword;
1558 }
1559
1560 /**
1561 * Returns the search word
1562 * @return string
1563 */
1564 public function getSword()
1565 {
1566 return $this->sword;
1567 }
1568 }