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