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