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