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