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