[BUGFIX] Do not call bidir_htmlspecialchars as static function
[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');
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 integer $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 integer $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 integer $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');
493 $cObj->setCurrentVal($row['sys_language_uid']);
494 $output = $cObj->cObjGetSingle($this->settings['flagRendering'], $this->settings['flagRendering.']);
495 } else {
496 // ... otherwise, get flag from sys_language record:
497 $languageRow = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('flag, title', 'sys_language', 'uid=' . (int)$row['sys_language_uid'] . $GLOBALS['TSFE']->cObj->enableFields('sys_language'));
498 // Flag code:
499 $flag = $languageRow['flag'];
500 if ($flag) {
501 // FIXME not all flags from typo3/gfx/flags
502 // are available in media/flags/
503 $file = \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix(PATH_tslib) . 'media/flags/flag_' . $flag;
504 $imgInfo = @getimagesize((PATH_site . $file));
505 if (is_array($imgInfo)) {
506 $output = '<img src="' . $file . '" ' . $imgInfo[3] . ' title="' . htmlspecialchars($languageRow['title']) . '" alt="' . htmlspecialchars($languageRow['title']) . '" />';
507 }
508 }
509 }
510 }
511 return $output;
512 }
513
514 /**
515 * Return icon for file extension
516 *
517 * @param string $imageType File extension / item type
518 * @param string $alt Title attribute value in icon.
519 * @param array $specRowConf TypoScript configuration specifically for search result.
520 * @return string <img> tag for icon
521 * @todo Define visibility
522 */
523 public function makeItemTypeIcon($imageType, $alt, $specRowConf) {
524 // Build compound key if item type is 0, iconRendering is not used
525 // and specConfs.[pid].pageIcon was set in TS
526 if ($imageType === '0' && $specRowConf['_pid'] && is_array($specRowConf['pageIcon.']) && !is_array($this->settings['iconRendering.'])) {
527 $imageType .= ':' . $specRowConf['_pid'];
528 }
529 if (!isset($this->iconFileNameCache[$imageType])) {
530 $this->iconFileNameCache[$imageType] = '';
531 // If TypoScript is used to render the icon:
532 if (is_array($this->settings['iconRendering.'])) {
533 $this->cObj->setCurrentVal($imageType);
534 $this->iconFileNameCache[$imageType] = $this->cObj->cObjGetSingle($this->settings['iconRendering'], $this->settings['iconRendering.']);
535 } else {
536 // Default creation / finding of icon:
537 $icon = '';
538 if ($imageType === '0' || substr($imageType, 0, 2) == '0:') {
539 if (is_array($specRowConf['pageIcon.'])) {
540 $this->iconFileNameCache[$imageType] = $this->cObj->IMAGE($specRowConf['pageIcon.']);
541 } else {
542 $icon = 'EXT:indexed_search/pi/res/pages.gif';
543 }
544 } elseif ($this->externalParsers[$imageType]) {
545 $icon = $this->externalParsers[$imageType]->getIcon($imageType);
546 }
547 if ($icon) {
548 $fullPath = GeneralUtility::getFileAbsFileName($icon);
549 if ($fullPath) {
550 $info = @getimagesize($fullPath);
551 $iconPath = \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix($fullPath);
552 $this->iconFileNameCache[$imageType] = is_array($info) ? '<img src="' . $iconPath . '" ' . $info[3] . ' title="' . htmlspecialchars($alt) . '" alt="" />' : '';
553 }
554 }
555 }
556 }
557 return $this->iconFileNameCache[$imageType];
558 }
559
560 /**
561 * Returns the resume for the search-result.
562 *
563 * @param array $row Search result row
564 * @param boolean $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.
565 * @param integer $length String length
566 * @return string HTML string
567 * @todo overwork this
568 */
569 protected function makeDescription($row, $noMarkup = FALSE, $length = 180) {
570 if ($row['show_resume']) {
571 if (!$noMarkup) {
572 $markedSW = '';
573 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'index_fulltext', 'phash=' . (int)$row['phash']);
574 if ($ftdrow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
575 // Cut HTTP references after some length
576 $content = preg_replace('/(http:\\/\\/[^ ]{60})([^ ]+)/i', '$1...', $ftdrow['fulltextdata']);
577 $markedSW = $this->markupSWpartsOfString($content);
578 }
579 $GLOBALS['TYPO3_DB']->sql_free_result($res);
580 }
581 if (!trim($markedSW)) {
582 $outputStr = $GLOBALS['TSFE']->csConvObj->crop('utf-8', $row['item_description'], $length);
583 $outputStr = htmlspecialchars($outputStr);
584 }
585 $output = $outputStr ?: $markedSW;
586 $output = $GLOBALS['TSFE']->csConv($output, 'utf-8');
587 } else {
588 $output = '<span class="noResume">' . \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('result.noResume', 'indexed_search') . '</span>';
589 }
590 return $output;
591 }
592
593 /**
594 * Marks up the search words from $this->searchWords in the $str with a color.
595 *
596 * @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.
597 * @return string Processed content
598 */
599 protected function markupSWpartsOfString($str) {
600 $htmlParser = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Html\\HtmlParser');
601 // Init:
602 $str = str_replace('&nbsp;', ' ', $htmlParser->bidir_htmlspecialchars($str, -1));
603 $str = preg_replace('/\\s\\s+/', ' ', $str);
604 $swForReg = array();
605 // Prepare search words for regex:
606 foreach ($this->searchWords as $d) {
607 $swForReg[] = preg_quote($d['sword'], '/');
608 }
609 $regExString = '(' . implode('|', $swForReg) . ')';
610 // Split and combine:
611 $parts = preg_split('/' . $regExString . '/i', ' ' . $str . ' ', 20000, PREG_SPLIT_DELIM_CAPTURE);
612 // Constants:
613 $summaryMax = 300;
614 $postPreLgd = 60;
615 $postPreLgd_offset = 5;
616 $divider = ' ... ';
617 $occurencies = (count($parts) - 1) / 2;
618 if ($occurencies) {
619 $postPreLgd = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($summaryMax / $occurencies, $postPreLgd, $summaryMax / 2);
620 }
621 // Variable:
622 $summaryLgd = 0;
623 $output = array();
624 // Shorten in-between strings:
625 foreach ($parts as $k => $strP) {
626 if ($k % 2 == 0) {
627 // Find length of the summary part:
628 $strLen = $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $parts[$k]);
629 $output[$k] = $parts[$k];
630 // Possibly shorten string:
631 if (!$k) {
632 // First entry at all (only cropped on the frontside)
633 if ($strLen > $postPreLgd) {
634 $output[$k] = $divider . preg_replace('/^[^[:space:]]+[[:space:]]/', '', $GLOBALS['TSFE']->csConvObj->crop('utf-8', $parts[$k], -($postPreLgd - $postPreLgd_offset)));
635 }
636 } elseif ($summaryLgd > $summaryMax || !isset($parts[($k + 1)])) {
637 // In case summary length is exceed OR if there are no more entries at all:
638 if ($strLen > $postPreLgd) {
639 $output[$k] = preg_replace('/[[:space:]][^[:space:]]+$/', '', $GLOBALS['TSFE']->csConvObj->crop('utf-8', $parts[$k], ($postPreLgd - $postPreLgd_offset))) . $divider;
640 }
641 } else {
642 if ($strLen > $postPreLgd * 2) {
643 $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)));
644 }
645 }
646 $summaryLgd += $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $output[$k]);
647 // Protect output:
648 $output[$k] = htmlspecialchars($output[$k]);
649 // If summary lgd is exceed, break the process:
650 if ($summaryLgd > $summaryMax) {
651 break;
652 }
653 } else {
654 $summaryLgd += $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $strP);
655 $output[$k] = '<strong class="tx-indexedsearch-redMarkup">' . htmlspecialchars($parts[$k]) . '</strong>';
656 }
657 }
658 // Return result:
659 return implode('', $output);
660 }
661
662 /**
663 * Write statistics information to database for the search operation
664 *
665 * @param array search params
666 * @param array Search Word array
667 * @param integer Number of hits
668 * @param integer Milliseconds the search took
669 * @return void
670 */
671 protected function writeSearchStat($searchParams, $searchWords, $count, $pt) {
672 $insertFields = array(
673 'searchstring' => $this->sword,
674 'searchoptions' => serialize(array($searchParams, $searchWords, $pt)),
675 'feuser_id' => (int)$GLOBALS['TSFE']->fe_user->user['uid'],
676 // cookie as set or retrieved. If people has cookies disabled this will vary all the time
677 'cookie' => $GLOBALS['TSFE']->fe_user->id,
678 // Remote IP address
679 'IP' => GeneralUtility::getIndpEnv('REMOTE_ADDR'),
680 // Number of hits on the search
681 'hits' => (int)$count,
682 // Time stamp
683 'tstamp' => $GLOBALS['EXEC_TIME']
684 );
685 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_search', $insertFields);
686 $newId = $GLOBALS['TYPO3_DB']->sql_insert_id();
687 if ($newId) {
688 foreach ($searchWords as $val) {
689 $insertFields = array(
690 'word' => $val['sword'],
691 'index_stat_search_id' => $newId,
692 // Time stamp
693 'tstamp' => $GLOBALS['EXEC_TIME'],
694 // search page id for indexed search stats
695 'pageid' => $GLOBALS['TSFE']->id
696 );
697 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_word', $insertFields);
698 }
699 }
700 }
701
702 /**
703 * 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)
704 *
705 * Only words with 2 or more characters are accepted
706 * Max 200 chars total
707 * Space is used to split words, "" can be used search for a whole string
708 * AND, OR and NOT are prefix words, overruling the default operator
709 * +/|/- equals AND, OR and NOT as operators.
710 * All search words are converted to lowercase.
711 *
712 * $defOp is the default operator. 1=OR, 0=AND
713 *
714 * @param boolean $defaultOperator If TRUE, the default operator will be OR, not AND
715 * @return array Search words if any found
716 */
717 protected function getSearchWords($defaultOperator) {
718 // 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!)
719 $searchWords = substr($this->sword, 0, 200);
720 // Convert to UTF-8 + conv. entities (was also converted during indexing!)
721 $searchWords = $GLOBALS['TSFE']->csConvObj->utf8_encode($searchWords, $GLOBALS['TSFE']->metaCharset);
722 $searchWords = $GLOBALS['TSFE']->csConvObj->entities_to_utf8($searchWords, TRUE);
723 $sWordArray = FALSE;
724 if ($hookObj = $this->hookRequest('getSearchWords')) {
725 $sWordArray = $hookObj->getSearchWords_splitSWords($searchWords, $defaultOperator);
726 } else {
727 // sentence
728 if ($this->searchDat['searchType'] == 20) {
729 $sWordArray = array(
730 array(
731 'sword' => trim($searchWords),
732 'oper' => 'AND'
733 )
734 );
735 } else {
736 // case-sensitive. Defines the words, which will be
737 // operators between words
738 $operatorTranslateTable = array(
739 array('+', 'AND'),
740 array('|', 'OR'),
741 array('-', 'AND NOT'),
742 // Add operators for various languages
743 // Converts the operators to UTF-8 and lowercase
744 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'),
745 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'),
746 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')
747 );
748 $search = GeneralUtility::makeInstance('TYPO3\\CMS\\Frontend\\ContentObject\\SearchResultContentObject');
749 $search->default_operator = $defaultOperator == 1 ? 'OR' : 'AND';
750 $search->operator_translate_table = $operatorTranslateTable;
751 $search->register_and_explode_search_string($searchWords);
752 if (is_array($search->sword_array)) {
753 $sWordArray = $this->procSearchWordsByLexer($search->sword_array);
754 }
755 }
756 }
757 return $sWordArray;
758 }
759
760 /**
761 * Post-process the search word array so it will match the words that was indexed (including case-folding if any)
762 * If any words are splitted into multiple words (eg. CJK will be!) the operator of the main word will remain.
763 *
764 * @param array $searchWords Search word array
765 * @return array Search word array, processed through lexer
766 */
767 protected function procSearchWordsByLexer($searchWords) {
768 $newSearchWords = array();
769 // Init lexer (used to post-processing of search words)
770 $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';
771 $this->lexerObj = GeneralUtility::getUserObj($lexerObjRef);
772 // Traverse the search word array
773 foreach ($searchWords as $wordDef) {
774 // No space in word (otherwise it might be a sentense in quotes like "there is").
775 if (strpos($wordDef['sword'], ' ') === FALSE) {
776 // Split the search word by lexer:
777 $res = $this->lexerObj->split2Words($wordDef['sword']);
778 // Traverse lexer result and add all words again:
779 foreach ($res as $word) {
780 $newSearchWords[] = array(
781 'sword' => $word,
782 'oper' => $wordDef['oper']
783 );
784 }
785 } else {
786 $newSearchWords[] = $wordDef;
787 }
788 }
789 return $newSearchWords;
790 }
791
792 /**
793 * Sort options about the search form
794 *
795 * @param array $search The search data / params
796 * @return void
797 * @dontvalidate $search
798 */
799 public function formAction($search = array()) {
800 $this->initialize($search);
801 // Adding search field value
802 $this->view->assign('sword', $this->sword);
803 // Additonal keyword => "Add to current search words"
804 $showAdditionalKeywordSearch = $this->settings['clearSearchBox'] && $this->settings['clearSearchBox.']['enableSubSearchCheckBox'];
805 if ($showAdditionalKeywordSearch) {
806 $this->view->assign('previousSearchWord', $this->settings['clearSearchBox'] ? '' : $this->sword);
807 }
808 $this->view->assign('showAdditionalKeywordSearch', $showAdditionalKeywordSearch);
809 // Extended search
810 if ($search['extendedSearch']) {
811 // "Search for"
812 $allSearchTypes = $this->getAllAvailableSearchTypeOptions();
813 $this->view->assign('allSearchTypes', $allSearchTypes);
814 $allDefaultOperands = $this->getAllAvailableOperandsOptions();
815 $this->view->assign('allDefaultOperands', $allDefaultOperands);
816 $showTypeSearch = count($allSearchTypes) || count($allDefaultOperands);
817 $this->view->assign('showTypeSearch', $showTypeSearch);
818 // "Search in"
819 $allMediaTypes = $this->getAllAvailableMediaTypesOptions();
820 $this->view->assign('allMediaTypes', $allMediaTypes);
821 $allLanguageUids = $this->getAllAvailableLanguageOptions();
822 $this->view->assign('allLanguageUids', $allLanguageUids);
823 $showMediaAndLanguageSearch = count($allMediaTypes) || count($allLanguageUids);
824 $this->view->assign('showMediaAndLanguageSearch', $showMediaAndLanguageSearch);
825 // Sections
826 $allSections = $this->getAllAvailableSectionsOptions();
827 $this->view->assign('allSections', $allSections);
828 // Free Indexing Configurations
829 $allIndexConfigurations = $this->getAllAvailableIndexConfigurationsOptions();
830 $this->view->assign('allIndexConfigurations', $allIndexConfigurations);
831 // Sorting
832 $allSortOrders = $this->getAllAvailableSortOrderOptions();
833 $this->view->assign('allSortOrders', $allSortOrders);
834 $allSortDescendings = $this->getAllAvailableSortDescendingOptions();
835 $this->view->assign('allSortDescendings', $allSortDescendings);
836 $showSortOrders = count($allSortOrders) || count($allSortDescendings);
837 $this->view->assign('showSortOrders', $showSortOrders);
838 // Limits
839 $allNumberOfResults = $this->getAllAvailableNumberOfResultsOptions();
840 $this->view->assign('allNumberOfResults', $allNumberOfResults);
841 $allGroups = $this->getAllAvailableGroupOptions();
842 $this->view->assign('allGroups', $allGroups);
843 }
844 }
845
846 /****************************************
847 * building together the available options for every dropdown
848 ***************************************/
849 /**
850 * get the values for the "type" selector
851 *
852 * @return array Associative array with options
853 */
854 protected function getAllAvailableSearchTypeOptions() {
855 $allOptions = array();
856 $types = array(0, 1, 2, 3, 10, 20);
857 $blindSettings = $this->settings['blind.'];
858 if (!$blindSettings['searchType']) {
859 foreach ($types as $typeNum) {
860 $allOptions[$typeNum] = \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('searchTypes.' . $typeNum, 'indexed_search');
861 }
862 }
863 // Remove this option if metaphone search is disabled)
864 if (!$this->enableMetaphoneSearch) {
865 unset($allOptions[10]);
866 }
867 // disable single entries by TypoScript
868 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['searchType.']);
869 return $allOptions;
870 }
871
872 /**
873 * get the values for the "defaultOperand" selector
874 *
875 * @return array Associative array with options
876 */
877 protected function getAllAvailableOperandsOptions() {
878 $allOptions = array();
879 $blindSettings = $this->settings['blind.'];
880 if (!$blindSettings['defaultOperand']) {
881 $allOptions = array(
882 0 => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('defaultOperands.0', 'indexed_search'),
883 1 => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('defaultOperands.1', 'indexed_search')
884 );
885 }
886 // disable single entries by TypoScript
887 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['defaultOperand.']);
888 return $allOptions;
889 }
890
891 /**
892 * get the values for the "media type" selector
893 *
894 * @return array Associative array with options
895 */
896 protected function getAllAvailableMediaTypesOptions() {
897 $allOptions = array();
898 $mediaTypes = array(-1, 0, -2);
899 $blindSettings = $this->settings['blind.'];
900 if (!$blindSettings['mediaType']) {
901 foreach ($mediaTypes as $mediaType) {
902 $allOptions[$mediaType] = \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('mediaTypes.' . $mediaType, 'indexed_search');
903 }
904 // Add media to search in:
905 $additionalMedia = trim($this->settings['mediaList']);
906 if (strlen($additionalMedia) > 0) {
907 $additionalMedia = GeneralUtility::trimExplode(',', $additionalMedia, TRUE);
908 }
909 foreach ($this->externalParsers as $extension => $obj) {
910 // Skip unwanted extensions
911 if (count($additionalMedia) && !in_array($extension, $additionalMedia)) {
912 continue;
913 }
914 if ($name = $obj->searchTypeMediaTitle($extension)) {
915 $allOptions[$extension] = \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('mediaTypes.' . $extension, $name);
916 }
917 }
918 }
919 // disable single entries by TypoScript
920 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['mediaType.']);
921 return $allOptions;
922 }
923
924 /**
925 * get the values for the "language" selector
926 *
927 * @return array Associative array with options
928 */
929 protected function getAllAvailableLanguageOptions() {
930 $allOptions = array(
931 '-1' => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('languageUids.-1', 'indexed_search'),
932 '0' => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('languageUids.0', 'indexed_search')
933 );
934 $blindSettings = $this->settings['blind.'];
935 if (!$blindSettings['languageUid']) {
936 // Add search languages
937 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'sys_language', '1=1' . $GLOBALS['TSFE']->cObj->enableFields('sys_language'));
938 if ($res) {
939 while ($lang = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
940 $allOptions[$lang['uid']] = $lang['title'];
941 }
942 }
943 // disable single entries by TypoScript
944 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['languageUid.']);
945 } else {
946 $allOptions = array();
947 }
948 return $allOptions;
949 }
950
951 /**
952 * get the values for the "section" selector
953 * Here values like "rl1_" and "rl2_" + a rootlevel 1/2 id can be added
954 * to perform searches in rootlevel 1+2 specifically. The id-values can even
955 * be commaseparated. Eg. "rl1_1,2" would search for stuff inside pages on
956 * menu-level 1 which has the uid's 1 and 2.
957 *
958 * @return array Associative array with options
959 */
960 protected function getAllAvailableSectionsOptions() {
961 $allOptions = array();
962 $sections = array(0, -1, -2, -3);
963 $blindSettings = $this->settings['blind.'];
964 if (!$blindSettings['sections']) {
965 foreach ($sections as $section) {
966 $allOptions[$section] = \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('sections.' . $section, 'indexed_search');
967 }
968 }
969 // Creating levels for section menu:
970 // This selects the first and secondary menus for the "sections" selector - so we can search in sections and sub sections.
971 if ($this->settings['displayLevel1Sections']) {
972 $firstLevelMenu = $this->getMenuOfPages($this->searchRootPageIdList);
973 $labelLevel1 = \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('sections.Rl1', 'indexed_search');
974 $labelLevel2 = \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('sections.Rl2', 'indexed_search');
975 foreach ($firstLevelMenu as $firstLevelKey => $menuItem) {
976 if (!$menuItem['nav_hide']) {
977 $allOptions['rl1_' . $menuItem['uid']] = trim($labelLevel1 . ' ' . $menuItem['title']);
978 if ($this->settings['displayLevel2Sections']) {
979 $secondLevelMenu = $this->getMenuOfPages($menuItem['uid']);
980 foreach ($secondLevelMenu as $secondLevelKey => $menuItemLevel2) {
981 if (!$menuItemLevel2['nav_hide']) {
982 $allOptions['rl2_' . $menuItemLevel2['uid']] = trim($labelLevel2 . ' ' . $menuItemLevel2['title']);
983 } else {
984 unset($secondLevelMenu[$secondLevelKey]);
985 }
986 }
987 $allOptions['rl2_' . implode(',', array_keys($secondLevelMenu))] = \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('sections.Rl2All', 'indexed_search');
988 }
989 } else {
990 unset($firstLevelMenu[$firstLevelKey]);
991 }
992 }
993 $allOptions['rl1_' . implode(',', array_keys($firstLevelMenu))] = \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('sections.Rl1All', 'indexed_search');
994 }
995 // disable single entries by TypoScript
996 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['sections.']);
997 return $allOptions;
998 }
999
1000 /**
1001 * get the values for the "freeIndexUid" selector
1002 *
1003 * @return array Associative array with options
1004 */
1005 protected function getAllAvailableIndexConfigurationsOptions() {
1006 $allOptions = array(
1007 '-1' => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('indexingConfigurations.-1', 'indexed_search'),
1008 '-2' => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('indexingConfigurations.-2', 'indexed_search'),
1009 '0' => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('indexingConfigurations.0', 'indexed_search')
1010 );
1011 $blindSettings = $this->settings['blind.'];
1012 if (!$blindSettings['indexingConfigurations']) {
1013 // add an additional index configuration
1014 if ($this->settings['defaultFreeIndexUidList']) {
1015 $uidList = GeneralUtility::intExplode(',', $this->settings['defaultFreeIndexUidList']);
1016 $indexCfgRecords = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid,title', 'index_config', 'uid IN (' . implode(',', $uidList) . ')' . $GLOBALS['TSFE']->cObj->enableFields('index_config'), '', '', '', 'uid');
1017 foreach ($uidList as $uidValue) {
1018 if (is_array($indexCfgRecords[$uidValue])) {
1019 $allOptions[$uidValue] = $indexCfgRecords[$uidValue]['title'];
1020 }
1021 }
1022 }
1023 // disable single entries by TypoScript
1024 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['indexingConfigurations.']);
1025 } else {
1026 $allOptions = array();
1027 }
1028 return $allOptions;
1029 }
1030
1031 /**
1032 * get the values for the "section" selector
1033 * Here values like "rl1_" and "rl2_" + a rootlevel 1/2 id can be added
1034 * to perform searches in rootlevel 1+2 specifically. The id-values can even
1035 * be commaseparated. Eg. "rl1_1,2" would search for stuff inside pages on
1036 * menu-level 1 which has the uid's 1 and 2.
1037 *
1038 * @return array Associative array with options
1039 */
1040 protected function getAllAvailableSortOrderOptions() {
1041 $allOptions = array();
1042 $sortOrders = array('rank_flag', 'rank_freq', 'rank_first', 'rank_count', 'mtime', 'title', 'crdate');
1043 $blindSettings = $this->settings['blind.'];
1044 if (!$blindSettings['sortOrder']) {
1045 foreach ($sortOrders as $order) {
1046 $allOptions[$order] = \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('sortOrders.' . $order, 'indexed_search');
1047 }
1048 }
1049 // disable single entries by TypoScript
1050 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['sortOrder.']);
1051 return $allOptions;
1052 }
1053
1054 /**
1055 * get the values for the "group" selector
1056 *
1057 * @return array Associative array with options
1058 */
1059 protected function getAllAvailableGroupOptions() {
1060 $allOptions = array();
1061 $blindSettings = $this->settings['blind.'];
1062 if (!$blindSettings['groupBy']) {
1063 $allOptions = array(
1064 'sections' => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('groupBy.sections', 'indexed_search'),
1065 'flat' => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('groupBy.flat', 'indexed_search')
1066 );
1067 }
1068 // disable single entries by TypoScript
1069 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['groupBy.']);
1070 return $allOptions;
1071 }
1072
1073 /**
1074 * get the values for the "sortDescending" selector
1075 *
1076 * @return array Associative array with options
1077 */
1078 protected function getAllAvailableSortDescendingOptions() {
1079 $allOptions = array();
1080 $blindSettings = $this->settings['blind.'];
1081 if (!$blindSettings['descending']) {
1082 $allOptions = array(
1083 0 => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('sortOrders.descending', 'indexed_search'),
1084 1 => \TYPO3\CMS\Extbase\Utility\LocalizationUtility::translate('sortOrders.ascending', 'indexed_search')
1085 );
1086 }
1087 // disable single entries by TypoScript
1088 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['descending.']);
1089 return $allOptions;
1090 }
1091
1092 /**
1093 * get the values for the "results" selector
1094 *
1095 * @return array Associative array with options
1096 */
1097 protected function getAllAvailableNumberOfResultsOptions() {
1098 $allOptions = array();
1099 $blindSettings = $this->settings['blind.'];
1100 if (!$blindSettings['numberOfResults']) {
1101 $allOptions = array(
1102 10 => 10,
1103 25 => 25,
1104 50 => 50,
1105 100 => 100
1106 );
1107 }
1108 // disable single entries by TypoScript
1109 $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['numberOfResults.']);
1110 return $allOptions;
1111 }
1112
1113 /**
1114 * removes blinding entries from the option list of a selector
1115 *
1116 * @param array $allOptions associative array containing all options
1117 * @param array $blindOptions associative array containing the optionkey as they key and the value = 1 if it should be removed
1118 * @return array Options from $allOptions with some options removed
1119 */
1120 protected function removeOptionsFromOptionList($allOptions, $blindOptions) {
1121 if (is_array($blindOptions)) {
1122 foreach ($blindOptions as $key => $val) {
1123 if ($val == 1) {
1124 unset($allOptions[$key]);
1125 }
1126 }
1127 }
1128 return $allOptions;
1129 }
1130
1131 /**
1132 * Links the $linkText to page $pageUid
1133 *
1134 * @param integer $pageUid Page id
1135 * @param string $linkText Title String to link
1136 * @param array $row Result row
1137 * @param array $markUpSwParams Additional parameters for marking up seach words
1138 * @return string <A> tag wrapped title string.
1139 * @todo make use of the UriBuilder
1140 */
1141 protected function linkPage($pageUid, $linkText, $row = array(), $markUpSwParams = array()) {
1142 // Parameters for link
1143 $urlParameters = (array) unserialize($row['cHashParams']);
1144 // Add &type and &MP variable:
1145 if ($row['data_page_mp']) {
1146 $urlParameters['MP'] = $row['data_page_mp'];
1147 }
1148 if ($row['sys_language_uid']) {
1149 $urlParameters['L'] = $row['sys_language_uid'];
1150 }
1151 // markup-GET vars:
1152 $urlParameters = array_merge($urlParameters, $markUpSwParams);
1153 // This will make sure that the path is retrieved if it hasn't been
1154 // already. Used only for the sake of the domain_record thing.
1155 if (!is_array($this->domainRecords[$pageUid])) {
1156 $this->getPathFromPageId($pageUid);
1157 }
1158 // If external domain, then link to that:
1159 if (count($this->domainRecords[$pageUid])) {
1160 $scheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://';
1161 $firstDomain = reset($this->domainRecords[$pageUid]);
1162 $additionalParams = '';
1163 if (is_array($urlParameters)) {
1164 if (count($urlParameters)) {
1165 $additionalParams = GeneralUtility::implodeArrayForUrl('', $urlParameters);
1166 }
1167 }
1168 $uri = $scheme . $firstDomain . '/index.php?id=' . $pageUid . $additionalParams;
1169 if ($target = $this->settings['detectDomainRecords.']['target']) {
1170 $target = ' target="' . $target . '"';
1171 }
1172 } else {
1173 $uriBuilder = $this->controllerContext->getUriBuilder();
1174 $uri = $uriBuilder->setTargetPageUid($pageUid)->setTargetPageType($row['data_page_type'])->setUseCacheHash(TRUE)->setArguments($urlParameters)->build();
1175 }
1176 return '<a href="' . htmlspecialchars($uri) . '"' . $target . '>' . htmlspecialchars($linkText) . '</a>';
1177 }
1178
1179 /**
1180 * Return the menu of pages used for the selector.
1181 *
1182 * @param integer $pageUid Page ID for which to return menu
1183 * @return array Menu items (for making the section selector box)
1184 */
1185 protected function getMenuOfPages($pageUid) {
1186 if ($this->settings['displayLevelxAllTypes']) {
1187 $menu = array();
1188 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('title,uid', 'pages', 'pid=' . (int)$pageUid . $GLOBALS['TSFE']->cObj->enableFields('pages'), '', 'sorting');
1189 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
1190 $menu[$row['uid']] = $GLOBALS['TSFE']->sys_page->getPageOverlay($row);
1191 }
1192 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1193 } else {
1194 $menu = $GLOBALS['TSFE']->sys_page->getMenu($pageUid);
1195 }
1196 return $menu;
1197 }
1198
1199 /**
1200 * Returns the path to the page $id
1201 *
1202 * @param integer $id Page ID
1203 * @param string MP variable content
1204 * @return string Path
1205 */
1206 protected function getPathFromPageId($id, $pathMP = '') {
1207 $identStr = $id . '|' . $pathMP;
1208 if (!isset($this->pathCache[$identStr])) {
1209 $this->requiredFrontendUsergroups[$id] = array();
1210 $this->domainRecords[$id] = array();
1211 $rl = $GLOBALS['TSFE']->sys_page->getRootLine($id, $pathMP);
1212 $path = '';
1213 if (is_array($rl) && count($rl)) {
1214 foreach ($rl as $k => $v) {
1215 // Check fe_user
1216 if ($v['fe_group'] && ($v['uid'] == $id || $v['extendToSubpages'])) {
1217 $this->requiredFrontendUsergroups[$id][] = $v['fe_group'];
1218 }
1219 // Check sys_domain
1220 if ($this->settings['detectDomainRcords']) {
1221 $domainName = $this->getFirstSysDomainRecordForPage($v['uid']);
1222 if ($domainName) {
1223 $this->domainRecords[$id][] = $domainName;
1224 // Set path accordingly
1225 $path = $domainName . $path;
1226 break;
1227 }
1228 }
1229 // Stop, if we find that the current id is the current root page.
1230 if ($v['uid'] == $GLOBALS['TSFE']->config['rootLine'][0]['uid']) {
1231 break;
1232 }
1233 $path = '/' . $v['title'] . $path;
1234 }
1235 }
1236 $this->pathCache[$identStr] = $path;
1237 }
1238 return $this->pathCache[$identStr];
1239 }
1240
1241 /**
1242 * Gets the first sys_domain record for the page, $id
1243 *
1244 * @param integer $id Page id
1245 * @return string Domain name
1246 */
1247 protected function getFirstSysDomainRecordForPage($id) {
1248 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('domainName', 'sys_domain', 'pid=' . (int)$id . $GLOBALS['TSFE']->cObj->enableFields('sys_domain'), '', 'sorting');
1249 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
1250 return rtrim($row['domainName'], '/');
1251 }
1252
1253 /**
1254 * simple function to initialize possible external parsers
1255 * feeds the $this->externalParsers array
1256 *
1257 * @return void
1258 */
1259 protected function initializeExternalParsers() {
1260 // Initialize external document parsers for icon display and other soft operations
1261 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['external_parsers'])) {
1262 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['external_parsers'] as $extension => $_objRef) {
1263 $this->externalParsers[$extension] = GeneralUtility::getUserObj($_objRef);
1264 // Init parser and if it returns FALSE, unset its entry again
1265 if (!$this->externalParsers[$extension]->softInit($extension)) {
1266 unset($this->externalParsers[$extension]);
1267 }
1268 }
1269 }
1270 }
1271
1272 /**
1273 * Returns an object reference to the hook object if any
1274 *
1275 * @param string $functionName Name of the function you want to call / hook key
1276 * @return object Hook object, if any. Otherwise NULL.
1277 */
1278 protected function hookRequest($functionName) {
1279 // Hook: menuConfig_preProcessModMenu
1280 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
1281 $hookObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
1282 if (method_exists($hookObj, $functionName)) {
1283 $hookObj->pObj = $this;
1284 return $hookObj;
1285 }
1286 }
1287 return NULL;
1288 }
1289
1290 /**
1291 * Returns if an item type is a multipage item type
1292 *
1293 * @param string $item_type Item type
1294 * @return boolean TRUE if multipage capable
1295 */
1296 protected function multiplePagesType($item_type) {
1297 return is_object($this->externalParsers[$item_type]) && $this->externalParsers[$item_type]->isMultiplePageExtension($item_type);
1298 }
1299
1300 }