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