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