1977b25b06acb2da922c702ba6712ad1b3e17b26
[Packages/TYPO3.CMS.git] / typo3 / sysext / indexed_search / Classes / Controller / SearchFormController.php
1 <?php
2 namespace TYPO3\CMS\IndexedSearch\Controller;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18
19 /**
20 * Index search frontend
21 *
22 * Creates a searchform for indexed search. Indexing must be enabled
23 * for this to make sense.
24 *
25 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
26 */
27 class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin {
28
29 public $prefixId = 'tx_indexedsearch';
30
31 // Same as class name
32 public $scriptRelPath = 'Classes/Controller/SearchFormController.php';
33
34 // Path to this script relative to the extension dir.
35 public $extKey = 'indexed_search';
36
37 // The extension key.
38 public $join_pages = 0;
39
40 // See document for info about this flag...
41 public $defaultResultNumber = 10;
42
43 public $operator_translate_table = array(array('+', 'AND'), array('|', 'OR'), array('-', 'AND NOT'));
44
45 // Internal variable
46 public $wholeSiteIdList = 0;
47
48 // Root-page PIDs to search in (rl0 field where clause, see initialize() function)
49 // Internals:
50 public $sWArr = array();
51
52 // Search Words and operators
53 public $optValues = array();
54
55 // Selector box values for search configuration form
56 public $firstRow = array();
57
58 // Will hold the first row in result - used to calculate relative hit-ratings.
59 public $cache_path = array();
60
61 // Caching of page path
62 public $cache_rl = array();
63
64 // Caching of root line data
65 public $fe_groups_required = array();
66
67 // Required fe_groups memberships for display of a result.
68 public $domain_records = array();
69
70 // Select clauses for individual words
71 public $wSelClauses = array();
72
73 // Domain records (?)
74 public $resultSections = array();
75
76 // Page tree sections for search result.
77 public $external_parsers = array();
78
79 // External parser objects
80 public $iconFileNameCache = array();
81
82 // Storage of icons....
83 public $templateCode;
84
85 // Will hold the content of $conf['templateFile']
86 public $hiddenFieldList = 'ext, type, defOp, media, order, group, lang, desc, results';
87
88 public $indexerConfig = array();
89
90 // Indexer configuration, coming from $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['indexed_search']
91 public $enableMetaphoneSearch = FALSE;
92
93 public $storeMetaphoneInfoAsWords;
94
95 /**
96 * Lexer object
97 *
98 * @var \TYPO3\CMS\IndexedSearch\Lexer
99 */
100 public $lexerObj;
101
102 const WILDCARD_LEFT = 1;
103 const WILDCARD_RIGHT = 2;
104 /**
105 * Main function, called from TypoScript as a USER_INT object.
106 *
107 * @param string Content input, ignore (just put blank string)
108 * @param array TypoScript configuration of the plugin!
109 * @return string HTML code for the search form / result display.
110 */
111 public function main($content, $conf) {
112 // Initialize:
113 $this->conf = $conf;
114 $this->pi_loadLL();
115 $this->pi_setPiVarDefaults();
116 // Initialize:
117 $this->initialize();
118 // Do search:
119 // If there were any search words entered...
120 if (is_array($this->sWArr)) {
121 $content = $this->doSearch($this->sWArr);
122 }
123 // Finally compile all the content, form, messages and results:
124 $content = $this->makeSearchForm($this->optValues) . $this->printRules() . $content;
125 return $this->pi_wrapInBaseClass($content);
126 }
127
128 /**
129 * Initialize internal variables, especially selector box values for the search form and search words
130 *
131 * @return void
132 */
133 public function initialize() {
134 // Indexer configuration from Extension Manager interface:
135 $this->indexerConfig = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['indexed_search']);
136 $this->enableMetaphoneSearch = $this->indexerConfig['enableMetaphoneSearch'] ? TRUE : FALSE;
137 $this->storeMetaphoneInfoAsWords = !\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_words');
138 // Initialize external document parsers for icon display and other soft operations
139 if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['external_parsers'])) {
140 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['external_parsers'] as $extension => $_objRef) {
141 $this->external_parsers[$extension] = GeneralUtility::getUserObj($_objRef);
142 // Init parser and if it returns FALSE, unset its entry again:
143 if (!$this->external_parsers[$extension]->softInit($extension)) {
144 unset($this->external_parsers[$extension]);
145 }
146 }
147 }
148 // Init lexer (used to post-processing of search words)
149 $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';
150 $this->lexerObj = GeneralUtility::getUserObj($lexerObjRef);
151 // If "_sections" is set, this value overrides any existing value.
152 if ($this->piVars['_sections']) {
153 $this->piVars['sections'] = $this->piVars['_sections'];
154 }
155 // If "_sections" is set, this value overrides any existing value.
156 if ($this->piVars['_freeIndexUid'] !== '_') {
157 $this->piVars['freeIndexUid'] = $this->piVars['_freeIndexUid'];
158 }
159 // Add previous search words to current
160 if ($this->piVars['sword_prev_include'] && $this->piVars['sword_prev']) {
161 $this->piVars['sword'] = trim($this->piVars['sword_prev']) . ' ' . $this->piVars['sword'];
162 }
163 $this->piVars['results'] = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($this->piVars['results'], 1, 100000, $this->defaultResultNumber);
164 // Selector-box values defined here:
165 $this->optValues = array(
166 'type' => array(
167 '0' => $this->pi_getLL('opt_type_0'),
168 '1' => $this->pi_getLL('opt_type_1'),
169 '2' => $this->pi_getLL('opt_type_2'),
170 '3' => $this->pi_getLL('opt_type_3'),
171 '10' => $this->pi_getLL('opt_type_10'),
172 '20' => $this->pi_getLL('opt_type_20')
173 ),
174 'defOp' => array(
175 '0' => $this->pi_getLL('opt_defOp_0'),
176 '1' => $this->pi_getLL('opt_defOp_1')
177 ),
178 'sections' => array(
179 '0' => $this->pi_getLL('opt_sections_0'),
180 '-1' => $this->pi_getLL('opt_sections_-1'),
181 '-2' => $this->pi_getLL('opt_sections_-2'),
182 '-3' => $this->pi_getLL('opt_sections_-3')
183 ),
184 'freeIndexUid' => array(
185 '-1' => $this->pi_getLL('opt_freeIndexUid_-1'),
186 '-2' => $this->pi_getLL('opt_freeIndexUid_-2'),
187 '0' => $this->pi_getLL('opt_freeIndexUid_0')
188 ),
189 'media' => array(
190 '-1' => $this->pi_getLL('opt_media_-1'),
191 '0' => $this->pi_getLL('opt_media_0'),
192 '-2' => $this->pi_getLL('opt_media_-2')
193 ),
194 'order' => array(
195 'rank_flag' => $this->pi_getLL('opt_order_rank_flag'),
196 'rank_freq' => $this->pi_getLL('opt_order_rank_freq'),
197 'rank_first' => $this->pi_getLL('opt_order_rank_first'),
198 'rank_count' => $this->pi_getLL('opt_order_rank_count'),
199 'mtime' => $this->pi_getLL('opt_order_mtime'),
200 'title' => $this->pi_getLL('opt_order_title'),
201 'crdate' => $this->pi_getLL('opt_order_crdate')
202 ),
203 'group' => array(
204 'sections' => $this->pi_getLL('opt_group_sections'),
205 'flat' => $this->pi_getLL('opt_group_flat')
206 ),
207 'lang' => array(
208 -1 => $this->pi_getLL('opt_lang_-1'),
209 0 => $this->pi_getLL('opt_lang_0')
210 ),
211 'desc' => array(
212 '0' => $this->pi_getLL('opt_desc_0'),
213 '1' => $this->pi_getLL('opt_desc_1')
214 ),
215 'results' => array(
216 '10' => '10',
217 '20' => '20',
218 '50' => '50',
219 '100' => '100'
220 )
221 );
222 // Remove this option if metaphone search is disabled)
223 if (!$this->enableMetaphoneSearch) {
224 unset($this->optValues['type']['10']);
225 }
226 // Free Index Uid:
227 if ($this->conf['search.']['defaultFreeIndexUidList']) {
228 $uidList = GeneralUtility::intExplode(',', $this->conf['search.']['defaultFreeIndexUidList']);
229 $indexCfgRecords = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid,title', 'index_config', 'uid IN (' . implode(',', $uidList) . ')' . $this->cObj->enableFields('index_config'), '', '', '', 'uid');
230 foreach ($uidList as $uidValue) {
231 if (is_array($indexCfgRecords[$uidValue])) {
232 $this->optValues['freeIndexUid'][$uidValue] = $indexCfgRecords[$uidValue]['title'];
233 }
234 }
235 }
236 // Should we use join_pages instead of long lists of uids?
237 if ($this->conf['search.']['skipExtendToSubpagesChecking']) {
238 $this->join_pages = 1;
239 }
240 // Add media to search in:
241 if (trim($this->conf['search.']['mediaList']) !== '') {
242 $mediaList = implode(',', GeneralUtility::trimExplode(',', $this->conf['search.']['mediaList'], TRUE));
243 }
244 foreach ($this->external_parsers as $extension => $obj) {
245 // Skip unwanted extensions
246 if ($mediaList && !GeneralUtility::inList($mediaList, $extension)) {
247 continue;
248 }
249 if ($name = $obj->searchTypeMediaTitle($extension)) {
250 $this->optValues['media'][$extension] = $this->pi_getLL('opt_sections_' . $extension, $name);
251 }
252 }
253 // Add operators for various languages
254 // Converts the operators to UTF-8 and lowercase
255 $this->operator_translate_table[] = array($GLOBALS['TSFE']->csConvObj->conv_case('utf-8', $GLOBALS['TSFE']->csConvObj->utf8_encode($this->pi_getLL('local_operator_AND'), $GLOBALS['TSFE']->renderCharset), 'toLower'), 'AND');
256 $this->operator_translate_table[] = array($GLOBALS['TSFE']->csConvObj->conv_case('utf-8', $GLOBALS['TSFE']->csConvObj->utf8_encode($this->pi_getLL('local_operator_OR'), $GLOBALS['TSFE']->renderCharset), 'toLower'), 'OR');
257 $this->operator_translate_table[] = array($GLOBALS['TSFE']->csConvObj->conv_case('utf-8', $GLOBALS['TSFE']->csConvObj->utf8_encode($this->pi_getLL('local_operator_NOT'), $GLOBALS['TSFE']->renderCharset), 'toLower'), 'AND NOT');
258 // This is the id of the site root. This value may be a commalist of integer (prepared for this)
259 $this->wholeSiteIdList = (int)$GLOBALS['TSFE']->config['rootLine'][0]['uid'];
260 // Creating levels for section menu:
261 // This selects the first and secondary menus for the "sections" selector - so we can search in sections and sub sections.
262 if ($this->conf['show.']['L1sections']) {
263 $firstLevelMenu = $this->getMenu($this->wholeSiteIdList);
264 foreach ($firstLevelMenu as $optionName => $mR) {
265 if (!$mR['nav_hide']) {
266 $this->optValues['sections']['rl1_' . $mR['uid']] = trim($this->pi_getLL('opt_RL1') . ' ' . $mR['title']);
267 if ($this->conf['show.']['L2sections']) {
268 $secondLevelMenu = $this->getMenu($mR['uid']);
269 foreach ($secondLevelMenu as $kk2 => $mR2) {
270 if (!$mR2['nav_hide']) {
271 $this->optValues['sections']['rl2_' . $mR2['uid']] = trim($this->pi_getLL('opt_RL2') . ' ' . $mR2['title']);
272 } else {
273 unset($secondLevelMenu[$kk2]);
274 }
275 }
276 $this->optValues['sections']['rl2_' . implode(',', array_keys($secondLevelMenu))] = $this->pi_getLL('opt_RL2ALL');
277 }
278 } else {
279 unset($firstLevelMenu[$optionName]);
280 }
281 }
282 $this->optValues['sections']['rl1_' . implode(',', array_keys($firstLevelMenu))] = $this->pi_getLL('opt_RL1ALL');
283 }
284 // Setting the list of root PIDs for the search. Notice, these page IDs MUST have a TypoScript template with root flag on them! Basically this list is used to select on the "rl0" field and page ids are registered as "rl0" only if a TypoScript template record with root flag is there.
285 // This happens AFTER the use of $this->wholeSiteIdList above because the above will then fetch the menu for the CURRENT site - regardless of this kind of searching here. Thus a general search will lookup in the WHOLE database while a specific section search will take the current sections...
286 if ($this->conf['search.']['rootPidList']) {
287 $this->wholeSiteIdList = implode(',', GeneralUtility::intExplode(',', $this->conf['search.']['rootPidList']));
288 }
289 // Load the template
290 $this->templateCode = $this->cObj->fileResource($this->conf['templateFile']);
291 // Add search languages:
292 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'sys_language', '1=1' . $this->cObj->enableFields('sys_language'));
293 while (FALSE !== ($data = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res))) {
294 $this->optValues['lang'][$data['uid']] = $data['title'];
295 }
296 $GLOBALS['TYPO3_DB']->sql_free_result($res);
297 // Calling hook for modification of initialized content
298 if ($hookObj = $this->hookRequest('initialize_postProc')) {
299 $hookObj->initialize_postProc();
300 }
301 // Default values set:
302 // Setting first values in optValues as default values IF there is not corresponding piVar value set already.
303 foreach ($this->optValues as $optionName => $optionValue) {
304 if (!isset($this->piVars[$optionName])) {
305 reset($optionValue);
306 $this->piVars[$optionName] = key($optionValue);
307 }
308 }
309 // Blind selectors:
310 if (is_array($this->conf['blind.'])) {
311 foreach ($this->conf['blind.'] as $optionName => $optionValue) {
312 if (is_array($optionValue)) {
313 foreach ($optionValue as $optionValueSubKey => $optionValueSubValue) {
314 if (!is_array($optionValueSubValue) && $optionValueSubValue && is_array($this->optValues[substr($optionName, 0, -1)])) {
315 unset($this->optValues[substr($optionName, 0, -1)][$optionValueSubKey]);
316 }
317 }
318 } elseif ($optionValue) {
319 // If value is not set, unset the option array
320 unset($this->optValues[$optionName]);
321 }
322 }
323 }
324 // This gets the search-words into the $sWArr:
325 $this->sWArr = $this->getSearchWords($this->piVars['defOp']);
326 }
327
328 /**
329 * 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)
330 *
331 * Only words with 2 or more characters are accepted
332 * Max 200 chars total
333 * Space is used to split words, "" can be used search for a whole string
334 * AND, OR and NOT are prefix words, overruling the default operator
335 * +/|/- equals AND, OR and NOT as operators.
336 * All search words are converted to lowercase.
337 *
338 * $defOp is the default operator. 1=OR, 0=AND
339 *
340 * @param bool If TRUE, the default operator will be OR, not AND
341 * @return array Returns array with search words if any found
342 */
343 public function getSearchWords($defOp) {
344 // 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!)
345 $inSW = substr($this->piVars['sword'], 0, 200);
346 // Convert to UTF-8 + conv. entities (was also converted during indexing!)
347 $inSW = $GLOBALS['TSFE']->csConvObj->utf8_encode($inSW, $GLOBALS['TSFE']->metaCharset);
348 $inSW = $GLOBALS['TSFE']->csConvObj->entities_to_utf8($inSW, TRUE);
349 $sWordArray = FALSE;
350 if ($hookObj = $this->hookRequest('getSearchWords')) {
351 $sWordArray = $hookObj->getSearchWords_splitSWords($inSW, $defOp);
352 } else {
353 if ($this->piVars['type'] == 20) {
354 // type = Sentence
355 $sWordArray = array(array('sword' => trim($inSW), 'oper' => 'AND'));
356 } else {
357 $search = GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\SearchResultContentObject::class);
358 $search->default_operator = $defOp == 1 ? 'OR' : 'AND';
359 $search->operator_translate_table = $this->operator_translate_table;
360 $search->register_and_explode_search_string($inSW);
361 if (is_array($search->sword_array)) {
362 $sWordArray = $this->procSearchWordsByLexer($search->sword_array);
363 }
364 }
365 }
366 return $sWordArray;
367 }
368
369 /**
370 * Post-process the search word array so it will match the words that was indexed (including case-folding if any)
371 * If any words are splitted into multiple words (eg. CJK will be!) the operator of the main word will remain.
372 *
373 * @param array Search word array
374 * @return array Search word array, processed through lexer
375 */
376 public function procSearchWordsByLexer($SWArr) {
377 // Init output variable:
378 $newSWArr = array();
379 // Traverse the search word array:
380 foreach ($SWArr as $wordDef) {
381 if (!strstr($wordDef['sword'], ' ')) {
382 // No space in word (otherwise it might be a sentense in quotes like "there is").
383 // Split the search word by lexer:
384 $res = $this->lexerObj->split2Words($wordDef['sword']);
385 // Traverse lexer result and add all words again:
386 foreach ($res as $word) {
387 $newSWArr[] = array('sword' => $word, 'oper' => $wordDef['oper']);
388 }
389 } else {
390 $newSWArr[] = $wordDef;
391 }
392 }
393 // Return result:
394 return $newSWArr;
395 }
396
397 /*****************************
398 *
399 * Main functions
400 *
401 *****************************/
402 /**
403 * Performs the search, the display and writing stats
404 *
405 * @param array Search words in array, see ->getSearchWords() for details
406 * @return string HTML for result display.
407 */
408 public function doSearch($sWArr) {
409 // Find free index uid:
410 $freeIndexUid = $this->piVars['freeIndexUid'];
411 if ($freeIndexUid == -2) {
412 $freeIndexUid = $this->conf['search.']['defaultFreeIndexUidList'];
413 }
414 $indexCfgs = GeneralUtility::intExplode(',', $freeIndexUid);
415 $accumulatedContent = '';
416 foreach ($indexCfgs as $freeIndexUid) {
417 // Get result rows:
418 $pt1 = GeneralUtility::milliseconds();
419 if ($hookObj = $this->hookRequest('getResultRows')) {
420 $resData = $hookObj->getResultRows($sWArr, $freeIndexUid);
421 } else {
422 $resData = $this->getResultRows($sWArr, $freeIndexUid);
423 }
424 // Display search results:
425 $pt2 = GeneralUtility::milliseconds();
426 if ($hookObj = $this->hookRequest('getDisplayResults')) {
427 $content = $hookObj->getDisplayResults($sWArr, $resData, $freeIndexUid);
428 } else {
429 $content = $this->getDisplayResults($sWArr, $resData, $freeIndexUid);
430 }
431 $pt3 = GeneralUtility::milliseconds();
432 // Create header if we are searching more than one indexing configuration:
433 if (count($indexCfgs) > 1) {
434 if ($freeIndexUid > 0) {
435 $indexCfgRec = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('title', 'index_config', 'uid=' . (int)$freeIndexUid . $this->cObj->enableFields('index_config'));
436 $titleString = $indexCfgRec['title'];
437 } else {
438 $titleString = $this->pi_getLL('opt_freeIndexUid_header_' . $freeIndexUid);
439 }
440 $content = '<h1 class="tx-indexedsearch-category">' . htmlspecialchars($titleString) . '</h1>' . $content;
441 }
442 $accumulatedContent .= $content;
443 }
444 // Write search statistics
445 $this->writeSearchStat($sWArr, $resData['count'], array($pt1, $pt2, $pt3));
446 // Return content:
447 return $accumulatedContent;
448 }
449
450 /**
451 * Get search result rows / data from database. Returned as data in array.
452 *
453 * @param array $searchWordArray Search word array
454 * @param int Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
455 * @return array False if no result, otherwise an array with keys for first row, result rows and total number of results found.
456 */
457 public function getResultRows($searchWordArray, $freeIndexUid = -1) {
458 // Getting SQL result pointer. This fetches ALL results (1,000,000 if found)
459 $GLOBALS['TT']->push('Searching result');
460 if ($hookObj = &$this->hookRequest('getResultRows_SQLpointer')) {
461 $res = $hookObj->getResultRows_SQLpointer($searchWordArray, $freeIndexUid);
462 } else {
463 $res = $this->getResultRows_SQLpointer($searchWordArray, $freeIndexUid);
464 }
465 $GLOBALS['TT']->pull();
466 // Organize and process result:
467 $result = FALSE;
468 if ($res) {
469 $totalSearchResultCount = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
470 // Total search-result count
471 $currentPageNumber = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($this->piVars['pointer'], 0, floor($totalSearchResultCount / $this->piVars['results']));
472 // The pointer is set to the result page that is currently being viewed
473 // Initialize result accumulation variables:
474 $positionInSearchResults = 0;
475 $groupingPhashes = array();
476 // Used for filtering out duplicates
477 $groupingChashes = array();
478 // Used for filtering out duplicates BASED ON cHash
479 $firstRow = array();
480 // Will hold the first row in result - used to calculate relative hit-ratings.
481 $resultRows = array();
482 // Will hold the results rows for display.
483 // Should we continue counting and checking of results even if
484 // we are sure they are not displayed in this request?
485 // This will slow down your page rendering, but it allows
486 // precise search result counters.
487 $calculateExactCount = (bool)$this->conf['search.']['exactCount'];
488 $lastResultNumberOnPreviousPage = $currentPageNumber * $this->piVars['results'];
489 $firstResultNumberOnNextPage = ($currentPageNumber + 1) * $this->piVars['results'];
490 $lastResultNumberToAnalyze = ($currentPageNumber + 1) * $this->piVars['results'] + $this->piVars['results'];
491 // Now, traverse result and put the rows to be displayed into an array
492 // Each row should contain the fields from 'ISEC.*, IP.*' combined + artificial fields "show_resume" (bool) and "result_number" (counter)
493 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res))) {
494 if (!$this->checkExistance($row)) {
495 // Check if the record is still available or if it has been deleted meanwhile.
496 // Currently this works for files only, since extending it to content elements would cause a lot of overhead...
497 // Otherwise, skip the row.
498 $totalSearchResultCount--;
499 continue;
500 }
501 // Set first row:
502 if ($positionInSearchResults === 0) {
503 $firstRow = $row;
504 }
505 $row['show_resume'] = $this->checkResume($row);
506 // Tells whether we can link directly to a document or not (depends on possible right problems)
507 $phashGr = !in_array($row['phash_grouping'], $groupingPhashes);
508 $chashGr = !in_array(($row['contentHash'] . '.' . $row['data_page_id']), $groupingChashes);
509 if ($phashGr && $chashGr) {
510 if ($row['show_resume'] || $this->conf['show.']['forbiddenRecords']) {
511 // Only if the resume may be shown are we going to filter out duplicates...
512 if (!$this->multiplePagesType($row['item_type'])) {
513 // Only on documents which are not multiple pages documents
514 $groupingPhashes[] = $row['phash_grouping'];
515 }
516 $groupingChashes[] = $row['contentHash'] . '.' . $row['data_page_id'];
517 $positionInSearchResults++;
518 // Check if we are inside result range for current page
519 if ($positionInSearchResults > $lastResultNumberOnPreviousPage && $positionInSearchResults <= $lastResultNumberToAnalyze) {
520 // Collect results to display
521 $row['result_number'] = $positionInSearchResults;
522 $resultRows[] = $row;
523 // This may lead to a problem: If the result
524 // check is not stopped here, the search will
525 // take longer. However the result counter
526 // will not filter out grouped cHashes/pHashes
527 // that were not processed yet. You can change
528 // this behavior using the "search.exactCount"
529 // property (see above).
530 $nextResultPosition = $positionInSearchResults + 1;
531 if (!$calculateExactCount && $nextResultPosition > $firstResultNumberOnNextPage) {
532 break;
533 }
534 }
535 } else {
536 // Skip this row if the user cannot view it (missing permission)
537 $totalSearchResultCount--;
538 }
539 } else {
540 // For each time a phash_grouping document is found
541 // (which is thus not displayed) the search-result count
542 // is reduced, so that it matches the number of rows displayed.
543 $totalSearchResultCount--;
544 }
545 }
546 $GLOBALS['TYPO3_DB']->sql_free_result($res);
547 $result = array(
548 'resultRows' => $resultRows,
549 'firstRow' => $firstRow,
550 'count' => $totalSearchResultCount
551 );
552 }
553 return $result;
554 }
555
556 /**
557 * Gets a SQL result pointer to traverse for the search records.
558 *
559 * @param array Search words
560 * @param int Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
561 * @return pointer
562 */
563 public function getResultRows_SQLpointer($sWArr, $freeIndexUid = -1) {
564 // This SEARCHES for the searchwords in $sWArr AND returns a COMPLETE list of phash-integers of the matches.
565 $list = $this->getPhashList($sWArr);
566 // Perform SQL Search / collection of result rows array:
567 if ($list) {
568 // Do the search:
569 $GLOBALS['TT']->push('execFinalQuery');
570 $res = $this->execFinalQuery($list, $freeIndexUid);
571 $GLOBALS['TT']->pull();
572 return $res;
573 } else {
574 return FALSE;
575 }
576 }
577
578 /**
579 * Compiles the HTML display of the incoming array of result rows.
580 *
581 * @param array Search words array (for display of text describing what was searched for)
582 * @param array Array with result rows, count, first row.
583 * @param int Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
584 * @return string HTML content to display result.
585 */
586 public function getDisplayResults($sWArr, $resData, $freeIndexUid = -1) {
587 // Perform display of result rows array:
588 if ($resData) {
589 $GLOBALS['TT']->push('Display Final result');
590 // Set first selected row (for calculation of ranking later)
591 $this->firstRow = $resData['firstRow'];
592 // Result display here:
593 $rowcontent = '';
594 $rowcontent .= $this->compileResult($resData['resultRows'], $freeIndexUid);
595 // Browsing box:
596 if ($resData['count']) {
597 $this->internal['res_count'] = $resData['count'];
598 $this->internal['results_at_a_time'] = $this->piVars['results'];
599 $this->internal['maxPages'] = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($this->conf['search.']['page_links'], 1, 100, 10);
600 $addString = $resData['count'] && $this->piVars['group'] == 'sections' && $freeIndexUid <= 0 ? ' ' . sprintf($this->pi_getLL((count($this->resultSections) > 1 ? 'inNsections' : 'inNsection')), count($this->resultSections)) : '';
601 $browseBox1 = $this->pi_list_browseresults(1, $addString, $this->printResultSectionLinks(), $freeIndexUid);
602 $browseBox2 = $this->pi_list_browseresults(0, '', '', $freeIndexUid);
603 }
604 // Browsing nav, bottom.
605 if ($resData['count']) {
606 $content = $browseBox1 . $rowcontent . $browseBox2;
607 } else {
608 $content = '<p' . $this->pi_classParam('noresults') . '>' . $this->pi_getLL('noResults', '', TRUE) . '</p>';
609 }
610 $GLOBALS['TT']->pull();
611 } else {
612 $content .= '<p' . $this->pi_classParam('noresults') . '>' . $this->pi_getLL('noResults', '', TRUE) . '</p>';
613 }
614 // Print a message telling which words we searched for, and in which sections etc.
615 $what = $this->tellUsWhatIsSeachedFor($sWArr) . (substr($this->piVars['sections'], 0, 2) == 'rl' ? ' ' . $this->pi_getLL('inSection', '', TRUE) . ' "' . substr($this->getPathFromPageId(substr($this->piVars['sections'], 4)), 1) . '"' : '');
616 $what = '<div' . $this->pi_classParam('whatis') . '>' . $this->cObj->stdWrap($what, $this->conf['whatis_stdWrap.']) . '</div>';
617 $content = $what . $content;
618 // Return content:
619 return $content;
620 }
621
622 /**
623 * Takes the array with resultrows as input and returns the result-HTML-code
624 * Takes the "group" var into account: Makes a "section" or "flat" display.
625 *
626 * @param array Result rows
627 * @param int Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
628 * @return string HTML
629 */
630 public function compileResult($resultRows, $freeIndexUid = -1) {
631 $content = '';
632 // Transfer result rows to new variable, performing some mapping of sub-results etc.
633 $newResultRows = array();
634 foreach ($resultRows as $row) {
635 $id = md5($row['phash_grouping']);
636 if (is_array($newResultRows[$id])) {
637 if (!$newResultRows[$id]['show_resume'] && $row['show_resume']) {
638 // swapping:
639 // Remove old
640 $subrows = $newResultRows[$id]['_sub'];
641 unset($newResultRows[$id]['_sub']);
642 $subrows[] = $newResultRows[$id];
643 // Insert new:
644 $newResultRows[$id] = $row;
645 $newResultRows[$id]['_sub'] = $subrows;
646 } else {
647 $newResultRows[$id]['_sub'][] = $row;
648 }
649 } else {
650 $newResultRows[$id] = $row;
651 }
652 }
653 $resultRows = $newResultRows;
654 $this->resultSections = array();
655 if ($freeIndexUid <= 0) {
656 switch ($this->piVars['group']) {
657 case 'sections':
658 $rl2flag = substr($this->piVars['sections'], 0, 2) == 'rl';
659 $sections = array();
660 foreach ($resultRows as $row) {
661 $id = $row['rl0'] . '-' . $row['rl1'] . ($rl2flag ? '-' . $row['rl2'] : '');
662 $sections[$id][] = $row;
663 }
664 $this->resultSections = array();
665 foreach ($sections as $id => $resultRows) {
666 $rlParts = explode('-', $id);
667 $theId = $rlParts[2] ? $rlParts[2] : ($rlParts[1] ? $rlParts[1] : $rlParts[0]);
668 $theRLid = $rlParts[2] ? 'rl2_' . $rlParts[2] : ($rlParts[1] ? 'rl1_' . $rlParts[1] : '0');
669 $sectionName = $this->getPathFromPageId($theId);
670 if ($sectionName[0] == '/') {
671 $sectionName = substr($sectionName, 1);
672 }
673 if (!trim($sectionName)) {
674 $sectionTitleLinked = $this->pi_getLL('unnamedSection', '', TRUE) . ':';
675 } elseif ($this->conf['linkSectionTitles']) {
676 $onclick = 'document.' . $this->prefixId . '[\'' . $this->prefixId . '[_sections]\'].value=\'' . $theRLid . '\';document.' . $this->prefixId . '.submit();return false;';
677 $sectionTitleLinked = '<a href="#" onclick="' . htmlspecialchars($onclick) . '">' . htmlspecialchars($sectionName) . ':</a>';
678 } else {
679 $sectionTitleLinked = htmlspecialchars($sectionName);
680 }
681 $this->resultSections[$id] = array($sectionName, count($resultRows));
682 // Add content header:
683 $content .= $this->makeSectionHeader($id, $sectionTitleLinked, count($resultRows));
684 // Render result rows:
685 $resultlist = '';
686 foreach ($resultRows as $row) {
687 $resultlist .= $this->printResultRow($row);
688 }
689 $content .= $this->cObj->stdWrap($resultlist, $this->conf['resultlist_stdWrap.']);
690 }
691 break;
692 default:
693 // flat:
694 $resultlist = '';
695 foreach ($resultRows as $row) {
696 $resultlist .= $this->printResultRow($row);
697 }
698 $content .= $this->cObj->stdWrap($resultlist, $this->conf['resultlist_stdWrap.']);
699 }
700 } else {
701 $resultlist = '';
702 foreach ($resultRows as $row) {
703 $resultlist .= $this->printResultRow($row);
704 }
705 $content .= $this->cObj->stdWrap($resultlist, $this->conf['resultlist_stdWrap.']);
706 }
707 return '<div' . $this->pi_classParam('res') . '>' . $content . '</div>';
708 }
709
710 /***********************************
711 *
712 * Searching functions (SQL)
713 *
714 ***********************************/
715 /**
716 * Returns a COMPLETE list of phash-integers matching the search-result composed of the search-words in the sWArr array.
717 * The list of phash integers are unsorted and should be used for subsequent selection of index_phash records for display of the result.
718 *
719 * @param array Search word array
720 * @return string List of integers
721 */
722 public function getPhashList($sWArr) {
723 // Initialize variables:
724 $c = 0;
725 $totalHashList = array();
726 // This array accumulates the phash-values
727 // Traverse searchwords; for each, select all phash integers and merge/diff/intersect them with previous word (based on operator)
728 foreach ($sWArr as $k => $v) {
729 // Making the query for a single search word based on the search-type
730 $sWord = $v['sword'];
731 $theType = (string)$this->piVars['type'];
732 if (strstr($sWord, ' ')) {
733 // If there are spaces in the search-word, make a full text search instead.
734 $theType = 20;
735 }
736 $GLOBALS['TT']->push('SearchWord "' . $sWord . '" - $theType=' . $theType);
737 // Perform search for word:
738 switch ($theType) {
739 case '1':
740 // Part of word
741 $res = $this->searchWord($sWord, self::WILDCARD_LEFT | self::WILDCARD_RIGHT);
742 break;
743 case '2':
744 // First part of word
745 $res = $this->searchWord($sWord, self::WILDCARD_RIGHT);
746 break;
747 case '3':
748 // Last part of word
749 $res = $this->searchWord($sWord, self::WILDCARD_LEFT);
750 break;
751 case '10':
752 // Sounds like
753 /**
754 * Indexer object
755 *
756 * @var \TYPO3\CMS\IndexedSearch\Indexer
757 */
758 // Initialize the indexer-class
759 $indexerObj = GeneralUtility::makeInstance(\TYPO3\CMS\IndexedSearch\Indexer::class);
760 // Perform metaphone search
761 $res = $this->searchMetaphone($indexerObj->metaphone($sWord, $this->storeMetaphoneInfoAsWords));
762 unset($indexerObj);
763 break;
764 case '20':
765 // Sentence
766 $res = $this->searchSentence($sWord);
767 $this->piVars['order'] = 'mtime';
768 // If there is a fulltext search for a sentence there is a likeliness that sorting cannot be done by the rankings from the rel-table (because no relations will exist for the sentence in the word-table). So therefore mtime is used instead. It is not required, but otherwise some hits may be left out.
769 break;
770 default:
771 // Distinct word
772 $res = $this->searchDistinct($sWord);
773 }
774 // If there was a query to do, then select all phash-integers which resulted from this.
775 if ($res) {
776 // Get phash list by searching for it:
777 $phashList = array();
778 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
779 $phashList[] = $row['phash'];
780 }
781 $GLOBALS['TYPO3_DB']->sql_free_result($res);
782 // Here the phash list are merged with the existing result based on whether we are dealing with OR, NOT or AND operations.
783 if ($c) {
784 switch ($v['oper']) {
785 case 'OR':
786 $totalHashList = array_unique(array_merge($phashList, $totalHashList));
787 break;
788 case 'AND NOT':
789 $totalHashList = array_diff($totalHashList, $phashList);
790 break;
791 default:
792 // AND...
793 $totalHashList = array_intersect($totalHashList, $phashList);
794 }
795 } else {
796 $totalHashList = $phashList;
797 }
798 }
799 $GLOBALS['TT']->pull();
800 $c++;
801 }
802 return implode(',', $totalHashList);
803 }
804
805 /**
806 * Returns a query which selects the search-word from the word/rel tables.
807 *
808 * @param string WHERE clause selecting the word from phash
809 * @param string Additional AND clause in the end of the query.
810 * @return pointer SQL result pointer
811 */
812 public function execPHashListQuery($wordSel, $plusQ = '') {
813 return $GLOBALS['TYPO3_DB']->exec_SELECTquery('IR.phash', 'index_words IW,
814 index_rel IR,
815 index_section ISEC', $wordSel . '
816 AND IW.wid=IR.wid
817 AND ISEC.phash = IR.phash
818 ' . $this->sectionTableWhere() . '
819 ' . $plusQ, 'IR.phash');
820 }
821
822 /**
823 * Search for a word
824 *
825 * @param string $sWord Word to search for
826 * @param int $mode Bit-field which can contain WILDCARD_LEFT and/or WILDCARD_RIGHT
827 * @return pointer SQL result pointer
828 */
829 public function searchWord($sWord, $mode) {
830 $wildcard_left = $mode & self::WILDCARD_LEFT ? '%' : '';
831 $wildcard_right = $mode & self::WILDCARD_RIGHT ? '%' : '';
832 $wSel = 'IW.baseword LIKE \'' . $wildcard_left . $GLOBALS['TYPO3_DB']->quoteStr($sWord, 'index_words') . $wildcard_right . '\'';
833 $this->wSelClauses[] = $wSel;
834 $res = $this->execPHashListQuery($wSel, ' AND is_stopword=0');
835 return $res;
836 }
837
838 /**
839 * Search for one distinct word
840 *
841 * @param string $sWord Word to search for
842 * @return pointer SQL result pointer
843 */
844 public function searchDistinct($sWord) {
845 $wSel = 'IW.wid=' . \TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::md5inthash($sWord);
846 $this->wSelClauses[] = $wSel;
847 $res = $this->execPHashListQuery($wSel, ' AND is_stopword=0');
848 return $res;
849 }
850
851 /**
852 * Search for a sentence
853 *
854 * @param string $sSentence Sentence to search for
855 * @return pointer SQL result pointer
856 */
857 public function searchSentence($sSentence) {
858 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('ISEC.phash', 'index_section ISEC, index_fulltext IFT', 'IFT.fulltextdata LIKE \'%' . $GLOBALS['TYPO3_DB']->quoteStr($sSentence, 'index_fulltext') . '%\' AND
859 ISEC.phash = IFT.phash
860 ' . $this->sectionTableWhere(), 'ISEC.phash');
861 $this->wSelClauses[] = '1=1';
862 return $res;
863 }
864
865 /**
866 * Search for a metaphone word
867 *
868 * @param string $sWord Word to search for
869 * @return \mysqli_result SQL result pointer
870 */
871 public function searchMetaphone($sWord) {
872 $wSel = 'IW.metaphone=' . $sWord;
873 $this->wSelClauses[] = $wSel;
874 return $this->execPHashListQuery($wSel, ' AND is_stopword=0');
875 }
876
877 /**
878 * Returns AND statement for selection of section in database. (rootlevel 0-2 + page_id)
879 *
880 * @return string AND clause for selection of section in database.
881 */
882 public function sectionTableWhere() {
883 $out = $this->wholeSiteIdList < 0 ? '' : ' AND ISEC.rl0 IN (' . $this->wholeSiteIdList . ')';
884 $match = '';
885 if (substr($this->piVars['sections'], 0, 4) == 'rl1_') {
886 $list = implode(',', GeneralUtility::intExplode(',', substr($this->piVars['sections'], 4)));
887 $out .= ' AND ISEC.rl1 IN (' . $list . ')';
888 $match = TRUE;
889 } elseif (substr($this->piVars['sections'], 0, 4) == 'rl2_') {
890 $list = implode(',', GeneralUtility::intExplode(',', substr($this->piVars['sections'], 4)));
891 $out .= ' AND ISEC.rl2 IN (' . $list . ')';
892 $match = TRUE;
893 } elseif (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'])) {
894 // Traversing user configured fields to see if any of those are used to limit search to a section:
895 foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['addRootLineFields'] as $fieldName => $rootLineLevel) {
896 if (substr($this->piVars['sections'], 0, strlen($fieldName) + 1) == $fieldName . '_') {
897 $list = implode(',', GeneralUtility::intExplode(',', substr($this->piVars['sections'], strlen($fieldName) + 1)));
898 $out .= ' AND ISEC.' . $fieldName . ' IN (' . $list . ')';
899 $match = TRUE;
900 break;
901 }
902 }
903 }
904 // If no match above, test the static types:
905 if (!$match) {
906 switch ((string)$this->piVars['sections']) {
907 case '-1':
908 // '-1' => 'Only this page',
909 $out .= ' AND ISEC.page_id=' . $GLOBALS['TSFE']->id;
910 break;
911 case '-2':
912 // '-2' => 'Top + level 1',
913 $out .= ' AND ISEC.rl2=0';
914 break;
915 case '-3':
916 // '-3' => 'Level 2 and out',
917 $out .= ' AND ISEC.rl2>0';
918 break;
919 }
920 }
921 return $out;
922 }
923
924 /**
925 * Returns AND statement for selection of media type
926 *
927 * @return string AND statement for selection of media type
928 */
929 public function mediaTypeWhere() {
930 switch ((string)$this->piVars['media']) {
931 case '0':
932 // '0' => 'Kun TYPO3 sider',
933 $out = ' AND IP.item_type=' . $GLOBALS['TYPO3_DB']->fullQuoteStr('0', 'index_phash');
934 break;
935 case '-2':
936 // All external documents
937 $out = ' AND IP.item_type<>' . $GLOBALS['TYPO3_DB']->fullQuoteStr('0', 'index_phash');
938 break;
939 case '-1':
940 // All content
941 $out = '';
942 break;
943 default:
944 $out = ' AND IP.item_type=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($this->piVars['media'], 'index_phash');
945 }
946 return $out;
947 }
948
949 /**
950 * Returns AND statement for selection of langauge
951 *
952 * @return string AND statement for selection of langauge
953 */
954 public function languageWhere() {
955 if ($this->piVars['lang'] >= 0) {
956 // -1 is the same as ALL language.
957 return 'AND IP.sys_language_uid=' . (int)$this->piVars['lang'];
958 }
959 }
960
961 /**
962 * Where-clause for free index-uid value.
963 *
964 * @param int Free Index UID value to limit search to.
965 * @return string WHERE SQL clause part.
966 */
967 public function freeIndexUidWhere($freeIndexUid) {
968 if ($freeIndexUid >= 0) {
969 // First, look if the freeIndexUid is a meta configuration:
970 $indexCfgRec = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('indexcfgs', 'index_config', 'type=5 AND uid=' . (int)$freeIndexUid . $this->cObj->enableFields('index_config'));
971 if (is_array($indexCfgRec)) {
972 $refs = GeneralUtility::trimExplode(',', $indexCfgRec['indexcfgs']);
973 $list = array(-99);
974 // Default value to protect against empty array.
975 foreach ($refs as $ref) {
976 list($table, $uid) = GeneralUtility::revExplode('_', $ref, 2);
977 switch ($table) {
978 case 'index_config':
979 $idxRec = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('uid', 'index_config', 'uid=' . (int)$uid . $this->cObj->enableFields('index_config'));
980 if ($idxRec) {
981 $list[] = $uid;
982 }
983 break;
984 case 'pages':
985 $indexCfgRecordsFromPid = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid', 'index_config', 'pid=' . (int)$uid . $this->cObj->enableFields('index_config'));
986 foreach ($indexCfgRecordsFromPid as $idxRec) {
987 $list[] = $idxRec['uid'];
988 }
989 break;
990 }
991 }
992 $list = array_unique($list);
993 } else {
994 $list = array((int)$freeIndexUid);
995 }
996 return ' AND IP.freeIndexUid IN (' . implode(',', $list) . ')';
997 }
998 }
999
1000 /**
1001 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
1002 *
1003 * @param string List of phash integers which match the search.
1004 * @param int Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
1005 * @return pointer Query result pointer
1006 */
1007 public function execFinalQuery($list, $freeIndexUid = -1) {
1008 // Setting up methods of filtering results based on page types, access, etc.
1009 $page_join = '';
1010 $page_where = '';
1011 // Indexing configuration clause:
1012 $freeIndexUidClause = $this->freeIndexUidWhere($freeIndexUid);
1013 // Calling hook for alternative creation of page ID list
1014 if ($hookObj = $this->hookRequest('execFinalQuery_idList')) {
1015 $page_where = $hookObj->execFinalQuery_idList($list);
1016 } elseif ($this->join_pages) {
1017 // Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
1018 $page_join = ',
1019 pages';
1020 $page_where = 'pages.uid = ISEC.page_id
1021 ' . $this->cObj->enableFields('pages') . '
1022 AND pages.no_search=0
1023 AND pages.doktype<200
1024 ';
1025 } elseif ($this->wholeSiteIdList >= 0) {
1026 // Collecting all pages IDs in which to search; filtering out ALL pages that are not accessible due to enableFields. Does NOT look for "no_search" field!
1027 $siteIdNumbers = GeneralUtility::intExplode(',', $this->wholeSiteIdList);
1028 $id_list = array();
1029 foreach ($siteIdNumbers as $rootId) {
1030 $id_list[] = $this->cObj->getTreeList(-1 * $rootId, 9999);
1031 }
1032 $page_where = ' ISEC.page_id IN (' . implode(',', $id_list) . ')';
1033 } else {
1034 // Disable everything... (select all)
1035 $page_where = ' 1=1 ';
1036 }
1037 // If any of the ranking sortings are selected, we must make a join with the word/rel-table again, because we need to calculate ranking based on all search-words found.
1038 if (substr($this->piVars['order'], 0, 5) == 'rank_') {
1039 switch ($this->piVars['order']) {
1040 case 'rank_flag':
1041 // This gives priority to word-position (max-value) so that words in title, keywords, description counts more than in content.
1042 // The ordering is refined with the frequency sum as well.
1043 $grsel = 'MAX(IR.flags) AS order_val1, SUM(IR.freq) AS order_val2';
1044 $orderBy = 'order_val1' . $this->isDescending() . ',order_val2' . $this->isDescending();
1045 break;
1046 case 'rank_first':
1047 // Results in average position of search words on page. Must be inversely sorted (low numbers are closer to top)
1048 $grsel = 'AVG(IR.first) AS order_val';
1049 $orderBy = 'order_val' . $this->isDescending(1);
1050 break;
1051 case 'rank_count':
1052 // Number of words found
1053 $grsel = 'SUM(IR.count) AS order_val';
1054 $orderBy = 'order_val' . $this->isDescending();
1055 break;
1056 default:
1057 // Frequency sum. I'm not sure if this is the best way to do it (make a sum...). Or should it be the average?
1058 $grsel = 'SUM(IR.freq) AS order_val';
1059 $orderBy = 'order_val' . $this->isDescending();
1060 }
1061
1062 // So, words are imploded into an OR statement (no "sentence search" should be done here - may deselect results)
1063 $wordSel = '(' . implode(' OR ', $this->wSelClauses) . ') AND ';
1064
1065 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
1066 'ISEC.*, IP.*, ' . $grsel,
1067 'index_words IW,
1068 index_rel IR,
1069 index_section ISEC,
1070 index_phash IP' . $page_join,
1071 $wordSel .
1072 'IP.phash IN (' . $list . ') ' .
1073 $this->mediaTypeWhere() . ' ' . $this->languageWhere() . $freeIndexUidClause . '
1074 AND IW.wid=IR.wid
1075 AND ISEC.phash = IR.phash
1076 AND IP.phash = IR.phash
1077 AND ' . $page_where,
1078 'IP.phash,ISEC.phash,ISEC.phash_t3,ISEC.rl0,ISEC.rl1,ISEC.rl2 ,ISEC.page_id,ISEC.uniqid,IP.phash_grouping,IP.data_filename ,IP.data_page_id ,IP.data_page_reg1,IP.data_page_type,IP.data_page_mp,IP.gr_list,IP.item_type,IP.item_title,IP.item_description,IP.item_mtime,IP.tstamp,IP.item_size,IP.contentHash,IP.crdate,IP.parsetime,IP.sys_language_uid,IP.item_crdate,IP.cHashParams,IP.externalUrl,IP.recordUid,IP.freeIndexUid,IP.freeIndexSetId',
1079 $orderBy
1080 );
1081 } else {
1082 // Otherwise, if sorting are done with the pages table or other fields, there is no need for joining with the rel/word tables:
1083 $orderBy = '';
1084 switch ((string)$this->piVars['order']) {
1085 case 'title':
1086 $orderBy = 'IP.item_title' . $this->isDescending();
1087 break;
1088 case 'crdate':
1089 $orderBy = 'IP.item_crdate' . $this->isDescending();
1090 break;
1091 case 'mtime':
1092 $orderBy = 'IP.item_mtime' . $this->isDescending();
1093 break;
1094 }
1095 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('ISEC.*, IP.*', 'index_phash IP,index_section ISEC' . $page_join, 'IP.phash IN (' . $list . ') ' . $this->mediaTypeWhere() . ' ' . $this->languageWhere() . $freeIndexUidClause . '
1096 AND IP.phash = ISEC.phash
1097 AND ' . $page_where, 'IP.phash,ISEC.phash,ISEC.phash_t3,ISEC.rl0,ISEC.rl1,ISEC.rl2 ,ISEC.page_id,ISEC.uniqid,IP.phash_grouping,IP.data_filename ,IP.data_page_id ,IP.data_page_reg1,IP.data_page_type,IP.data_page_mp,IP.gr_list,IP.item_type,IP.item_title,IP.item_description,IP.item_mtime,IP.tstamp,IP.item_size,IP.contentHash,IP.crdate,IP.parsetime,IP.sys_language_uid,IP.item_crdate,IP.cHashParams,IP.externalUrl,IP.recordUid,IP.freeIndexUid,IP.freeIndexSetId', $orderBy);
1098 }
1099 return $res;
1100 }
1101
1102 /**
1103 * Checking if the resume can be shown for the search result (depending on whether the rights are OK)
1104 * ? Should it also check for gr_list "0,-1"?
1105 *
1106 * @param array Result row array.
1107 * @return bool Returns TRUE if resume can safely be shown
1108 */
1109 public function checkResume($row) {
1110 // If the record is indexed by an indexing configuration, just show it.
1111 // At least this is needed for external URLs and files.
1112 // For records we might need to extend this - for instance block display if record is access restricted.
1113 if ($row['freeIndexUid']) {
1114 return TRUE;
1115 }
1116 // Evaluate regularly indexed pages based on item_type:
1117 if ($row['item_type']) {
1118 // External media:
1119 // For external media we will check the access of the parent page on which the media was linked from.
1120 // "phash_t3" is the phash of the parent TYPO3 page row which initiated the indexing of the documents in this section.
1121 // So, selecting for the grlist records belonging to the parent phash-row where the current users gr_list exists will help us to know.
1122 // If this is NOT found, there is still a theoretical possibility that another user accessible page would display a link, so maybe the resume of such a document here may be unjustified hidden. But better safe than sorry.
1123 if (\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_grlist')) {
1124 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('phash', 'index_grlist', 'phash=' . (int)$row['phash_t3'] . ' AND gr_list=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($GLOBALS['TSFE']->gr_list, 'index_grlist'));
1125 } else {
1126 $res = FALSE;
1127 }
1128 if ($res && $GLOBALS['TYPO3_DB']->sql_num_rows($res)) {
1129 return TRUE;
1130 } else {
1131 return FALSE;
1132 }
1133 } else {
1134 // Ordinary TYPO3 pages:
1135 if ((string)$row['gr_list'] !== (string)$GLOBALS['TSFE']->gr_list) {
1136 // Selecting for the grlist records belonging to the phash-row where the current users gr_list exists. If it is found it is proof that this user has direct access to the phash-rows content although he did not himself initiate the indexing...
1137 if (\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_grlist')) {
1138 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('phash', 'index_grlist', 'phash=' . (int)$row['phash'] . ' AND gr_list=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($GLOBALS['TSFE']->gr_list, 'index_grlist'));
1139 } else {
1140 $res = FALSE;
1141 }
1142 if ($res && $GLOBALS['TYPO3_DB']->sql_num_rows($res)) {
1143 return TRUE;
1144 } else {
1145 return FALSE;
1146 }
1147 } else {
1148 return TRUE;
1149 }
1150 }
1151 }
1152
1153 /**
1154 * Check if the record is still available or if it has been deleted meanwhile.
1155 * Currently this works for files only, since extending it to page content would cause a lot of overhead.
1156 *
1157 * @param array Result row array
1158 * @return bool Returns TRUE if record is still available
1159 */
1160 public function checkExistance($row) {
1161 $recordExists = TRUE;
1162 // Always expect that page content exists
1163 if ($row['item_type']) {
1164 // External media:
1165 if (!is_file($row['data_filename']) || !file_exists($row['data_filename'])) {
1166 $recordExists = FALSE;
1167 }
1168 }
1169 return $recordExists;
1170 }
1171
1172 /**
1173 * Returns "DESC" or "" depending on the settings of the incoming highest/lowest result order (piVars['desc']
1174 *
1175 * @param bool If TRUE, inverse the order which is defined by piVars['desc']
1176 * @return string " DESC" or
1177 */
1178 public function isDescending($inverse = FALSE) {
1179 $desc = $this->piVars['desc'];
1180 if ($inverse) {
1181 $desc = !$desc;
1182 }
1183 return !$desc ? ' DESC' : '';
1184 }
1185
1186 /**
1187 * Write statistics information to database for the search operation
1188 *
1189 * @param array Search Word array
1190 * @param int Number of hits
1191 * @param int Milliseconds the search took
1192 * @return void
1193 */
1194 public function writeSearchStat($sWArr, $count, $pt) {
1195 $insertFields = array(
1196 'searchstring' => $this->piVars['sword'],
1197 'searchoptions' => serialize(array($this->piVars, $sWArr, $pt)),
1198 'feuser_id' => (int)$this->fe_user->user['uid'],
1199 // fe_user id, integer
1200 'cookie' => (string)$this->fe_user->id,
1201 // cookie as set or retrieve. If people has cookies disabled this will vary all the time...
1202 'IP' => GeneralUtility::getIndpEnv('REMOTE_ADDR'),
1203 // Remote IP address
1204 'hits' => (int)$count,
1205 // Number of hits on the search.
1206 'tstamp' => $GLOBALS['EXEC_TIME']
1207 );
1208 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_search', $insertFields);
1209 $newId = $GLOBALS['TYPO3_DB']->sql_insert_id();
1210 if ($newId) {
1211 foreach ($sWArr as $val) {
1212 $insertFields = array(
1213 'word' => $val['sword'],
1214 'index_stat_search_id' => $newId,
1215 'tstamp' => $GLOBALS['EXEC_TIME'],
1216 // Time stamp
1217 'pageid' => $GLOBALS['TSFE']->id
1218 );
1219 $GLOBALS['TYPO3_DB']->exec_INSERTquery('index_stat_word', $insertFields);
1220 }
1221 }
1222 }
1223
1224 /***********************************
1225 *
1226 * HTML output functions
1227 *
1228 ***********************************/
1229 /**
1230 * Make search form HTML
1231 *
1232 * @param array Value/Labels pairs for search form selector boxes.
1233 * @return string Search form HTML
1234 */
1235 public function makeSearchForm($optValues) {
1236 $html = $this->cObj->getSubpart($this->templateCode, '###SEARCH_FORM###');
1237 // Multilangual text
1238 $substituteArray = array('legend', 'searchFor', 'extResume', 'atATime', 'orderBy', 'fromSection', 'searchIn', 'match', 'style', 'freeIndexUid');
1239 foreach ($substituteArray as $marker) {
1240 $markerArray['###FORM_' . GeneralUtility::strtoupper($marker) . '###'] = $this->pi_getLL('form_' . $marker, '', TRUE);
1241 }
1242 $markerArray['###FORM_SUBMIT###'] = $this->pi_getLL('submit_button_label', '', TRUE);
1243 // Adding search field value
1244 $markerArray['###SWORD_VALUE###'] = htmlspecialchars($this->piVars['sword']);
1245 // Additonal keyword => "Add to current search words"
1246 if ($this->conf['show.']['clearSearchBox'] && $this->conf['show.']['clearSearchBox.']['enableSubSearchCheckBox']) {
1247 $markerArray['###SWORD_PREV_VALUE###'] = htmlspecialchars($this->conf['show.']['clearSearchBox'] ? '' : $this->piVars['sword']);
1248 $markerArray['###SWORD_PREV_INCLUDE_CHECKED###'] = $this->piVars['sword_prev_include'] ? ' checked="checked"' : '';
1249 $markerArray['###ADD_TO_CURRENT_SEARCH###'] = $this->pi_getLL('makerating_addToCurrentSearch', '', TRUE);
1250 } else {
1251 $html = $this->cObj->substituteSubpart($html, '###ADDITONAL_KEYWORD###', '');
1252 }
1253 $markerArray['###ACTION_URL###'] = htmlspecialchars($this->getSearchFormActionURL());
1254 $hiddenFieldCode = $this->cObj->getSubpart($this->templateCode, '###HIDDEN_FIELDS###');
1255 $hiddenFieldCode = preg_replace('/^\\n\\t(.+)/ms', '$1', $hiddenFieldCode);
1256 // Remove first newline and tab (cosmetical issue)
1257 $hiddenFieldArr = array();
1258 foreach (GeneralUtility::trimExplode(',', $this->hiddenFieldList) as $fieldName) {
1259 $hiddenFieldMarkerArray = array();
1260 $hiddenFieldMarkerArray['###HIDDEN_FIELDNAME###'] = $this->prefixId . '[' . $fieldName . ']';
1261 $hiddenFieldMarkerArray['###HIDDEN_VALUE###'] = htmlspecialchars((string)$this->piVars[$fieldName]);
1262 $hiddenFieldArr[$fieldName] = $this->cObj->substituteMarkerArrayCached($hiddenFieldCode, $hiddenFieldMarkerArray, array(), array());
1263 }
1264 // Extended search
1265 if ($this->piVars['ext']) {
1266 // Search for
1267 if (!is_array($optValues['type']) && !is_array($optValues['defOp']) || $this->conf['blind.']['type'] && $this->conf['blind.']['defOp']) {
1268 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_FOR###', '');
1269 } else {
1270 if (is_array($optValues['type']) && !$this->conf['blind.']['type']) {
1271 unset($hiddenFieldArr['type']);
1272 $markerArray['###SELECTBOX_TYPE_VALUES###'] = $this->renderSelectBoxValues($this->piVars['type'], $optValues['type']);
1273 } else {
1274 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_TYPE###', '');
1275 }
1276 if (is_array($optValues['defOp']) || !$this->conf['blind.']['defOp']) {
1277 $markerArray['###SELECTBOX_DEFOP_VALUES###'] = $this->renderSelectBoxValues($this->piVars['defOp'], $optValues['defOp']);
1278 } else {
1279 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_DEFOP###', '');
1280 }
1281 }
1282 // Search in
1283 if (!is_array($optValues['media']) && !is_array($optValues['lang']) || $this->conf['blind.']['media'] && $this->conf['blind.']['lang']) {
1284 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_IN###', '');
1285 } else {
1286 if (is_array($optValues['media']) && !$this->conf['blind.']['media']) {
1287 unset($hiddenFieldArr['media']);
1288 $markerArray['###SELECTBOX_MEDIA_VALUES###'] = $this->renderSelectBoxValues($this->piVars['media'], $optValues['media']);
1289 } else {
1290 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_MEDIA###', '');
1291 }
1292 if (is_array($optValues['lang']) || !$this->conf['blind.']['lang']) {
1293 unset($hiddenFieldArr['lang']);
1294 $markerArray['###SELECTBOX_LANG_VALUES###'] = $this->renderSelectBoxValues($this->piVars['lang'], $optValues['lang']);
1295 } else {
1296 $html = $this->cObj->substituteSubpart($html, '###SELECT_SEARCH_LANG###', '');
1297 }
1298 }
1299 // Sections
1300 if (!is_array($optValues['sections']) || $this->conf['blind.']['sections']) {
1301 $html = $this->cObj->substituteSubpart($html, '###SELECT_SECTION###', '');
1302 } else {
1303 $markerArray['###SELECTBOX_SECTIONS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['sections'], $optValues['sections']);
1304 }
1305 // Free Indexing Configurations:
1306 if (!is_array($optValues['freeIndexUid']) || $this->conf['blind.']['freeIndexUid']) {
1307 $html = $this->cObj->substituteSubpart($html, '###SELECT_FREEINDEXUID###', '');
1308 } else {
1309 $markerArray['###SELECTBOX_FREEINDEXUIDS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['freeIndexUid'], $optValues['freeIndexUid']);
1310 }
1311 // Sorting
1312 if (!is_array($optValues['order']) || !is_array($optValues['desc']) || $this->conf['blind.']['order']) {
1313 $html = $this->cObj->substituteSubpart($html, '###SELECT_ORDER###', '');
1314 } else {
1315 unset($hiddenFieldArr['order']);
1316 unset($hiddenFieldArr['desc']);
1317 unset($hiddenFieldArr['results']);
1318 $markerArray['###SELECTBOX_ORDER_VALUES###'] = $this->renderSelectBoxValues($this->piVars['order'], $optValues['order']);
1319 $markerArray['###SELECTBOX_DESC_VALUES###'] = $this->renderSelectBoxValues($this->piVars['desc'], $optValues['desc']);
1320 $markerArray['###SELECTBOX_RESULTS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['results'], $optValues['results']);
1321 }
1322 // Limits
1323 if (!is_array($optValues['results']) || !is_array($optValues['results']) || $this->conf['blind.']['results']) {
1324 $html = $this->cObj->substituteSubpart($html, '###SELECT_RESULTS###', '');
1325 } else {
1326 $markerArray['###SELECTBOX_RESULTS_VALUES###'] = $this->renderSelectBoxValues($this->piVars['results'], $optValues['results']);
1327 }
1328 // Grouping
1329 if (!is_array($optValues['group']) || $this->conf['blind.']['group']) {
1330 $html = $this->cObj->substituteSubpart($html, '###SELECT_GROUP###', '');
1331 } else {
1332 unset($hiddenFieldArr['group']);
1333 $markerArray['###SELECTBOX_GROUP_VALUES###'] = $this->renderSelectBoxValues($this->piVars['group'], $optValues['group']);
1334 }
1335 if ($this->conf['blind.']['extResume']) {
1336 $html = $this->cObj->substituteSubpart($html, '###SELECT_EXTRESUME###', '');
1337 } else {
1338 $markerArray['###EXT_RESUME_CHECKED###'] = $this->piVars['extResume'] ? ' checked="checked"' : '';
1339 }
1340 } else {
1341 // Extended search
1342 $html = $this->cObj->substituteSubpart($html, '###SEARCH_FORM_EXTENDED###', '');
1343 }
1344 if ($this->conf['show.']['advancedSearchLink']) {
1345 $linkToOtherMode = $this->piVars['ext'] ? $this->pi_getPageLink($GLOBALS['TSFE']->id, $GLOBALS['TSFE']->sPre) : $this->pi_getPageLink($GLOBALS['TSFE']->id, $GLOBALS['TSFE']->sPre, array($this->prefixId . '[ext]' => 1));
1346 $markerArray['###LINKTOOTHERMODE###'] = '<a href="' . htmlspecialchars($linkToOtherMode) . '">' . $this->pi_getLL(($this->piVars['ext'] ? 'link_regularSearch' : 'link_advancedSearch'), '', TRUE) . '</a>';
1347 } else {
1348 $markerArray['###LINKTOOTHERMODE###'] = '';
1349 }
1350 // Write all hidden fields
1351 $html = $this->cObj->substituteSubpart($html, '###HIDDEN_FIELDS###', implode('', $hiddenFieldArr));
1352 $substitutedContent = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1353 return $substitutedContent;
1354 }
1355
1356 /**
1357 * Function, rendering selector box values.
1358 *
1359 * @param string Current value
1360 * @param array Array with the options as key=>value pairs
1361 * @return string <options> imploded.
1362 */
1363 public function renderSelectBoxValues($value, $optValues) {
1364 if (is_array($optValues)) {
1365 $opt = array();
1366 $isSelFlag = 0;
1367 foreach ($optValues as $k => $v) {
1368 $sel = (string)$k === (string)$value ? ' selected="selected"' : '';
1369 if ($sel) {
1370 $isSelFlag++;
1371 }
1372 $opt[] = '<option value="' . htmlspecialchars($k) . '"' . $sel . '>' . htmlspecialchars($v) . '</option>';
1373 }
1374 return implode('', $opt);
1375 }
1376 }
1377
1378 /**
1379 * Print the searching rules
1380 *
1381 * @return string Rules for the search
1382 */
1383 public function printRules() {
1384 if ($this->conf['show.']['rules']) {
1385 $html = $this->cObj->getSubpart($this->templateCode, '###RULES###');
1386 $markerArray['###RULES_HEADER###'] = $this->pi_getLL('rules_header', '', TRUE);
1387 $markerArray['###RULES_TEXT###'] = nl2br(trim($this->pi_getLL('rules_text', '', TRUE)));
1388 $substitutedContent = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1389 return $this->cObj->stdWrap($substitutedContent, $this->conf['rules_stdWrap.']);
1390 }
1391 }
1392
1393 /**
1394 * Returns the anchor-links to the sections inside the displayed result rows.
1395 *
1396 * @return string
1397 */
1398 public function printResultSectionLinks() {
1399 if (count($this->resultSections)) {
1400 $lines = array();
1401 $html = $this->cObj->getSubpart($this->templateCode, '###RESULT_SECTION_LINKS###');
1402 $item = $this->cObj->getSubpart($this->templateCode, '###RESULT_SECTION_LINKS_LINK###');
1403 foreach ($this->resultSections as $id => $dat) {
1404 $markerArray = array();
1405 $aBegin = '<a href="' . htmlspecialchars(($GLOBALS['TSFE']->anchorPrefix . '#anchor_' . md5($id))) . '">';
1406 $aContent = htmlspecialchars((trim($dat[0]) ? trim($dat[0]) : $this->pi_getLL('unnamedSection'))) . ' (' . $dat[1] . ' ' . $this->pi_getLL(($dat[1] > 1 ? 'word_pages' : 'word_page'), '', TRUE) . ')';
1407 $aEnd = '</a>';
1408 $markerArray['###LINK###'] = $aBegin . $aContent . $aEnd;
1409 $links[] = $this->cObj->substituteMarkerArrayCached($item, $markerArray, array(), array());
1410 }
1411 $html = $this->cObj->substituteMarkerArrayCached($html, array('###LINKS###' => implode('', $links)), array(), array());
1412 return '<div' . $this->pi_classParam('sectionlinks') . '>' . $this->cObj->stdWrap($html, $this->conf['sectionlinks_stdWrap.']) . '</div>';
1413 }
1414 }
1415
1416 /**
1417 * Returns the section header of the search result.
1418 *
1419 * @param string ID for the section (used for anchor link)
1420 * @param string Section title with linked wrapped around
1421 * @param int Number of results in section
1422 * @return string HTML output
1423 */
1424 public function makeSectionHeader($id, $sectionTitleLinked, $countResultRows) {
1425 $html = $this->cObj->getSubpart($this->templateCode, '###SECTION_HEADER###');
1426 $markerArray['###ANCHOR_URL###'] = 'anchor_' . md5($id);
1427 $markerArray['###SECTION_TITLE###'] = $sectionTitleLinked;
1428 $markerArray['###RESULT_COUNT###'] = $countResultRows;
1429 $markerArray['###RESULT_NAME###'] = $this->pi_getLL('word_page' . ($countResultRows > 1 ? 's' : ''));
1430 $substitutedContent = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1431 return $substitutedContent;
1432 }
1433
1434 /**
1435 * This prints a single result row, including a recursive call for subrows.
1436 *
1437 * @param array Search result row
1438 * @param int 1=Display only header (for sub-rows!), 2=nothing at all
1439 * @return string HTML code
1440 */
1441 public function printResultRow($row, $headerOnly = 0) {
1442 // Get template content:
1443 $tmplContent = $this->prepareResultRowTemplateData($row, $headerOnly);
1444 if ($hookObj = $this->hookRequest('printResultRow')) {
1445 return $hookObj->printResultRow($row, $headerOnly, $tmplContent);
1446 } else {
1447 $html = $this->cObj->getSubpart($this->templateCode, '###RESULT_OUTPUT###');
1448 if (!is_array($row['_sub'])) {
1449 $html = $this->cObj->substituteSubpart($html, '###ROW_SUB###', '');
1450 }
1451 if (!$headerOnly) {
1452 $html = $this->cObj->substituteSubpart($html, '###ROW_SHORT###', '');
1453 } elseif ($headerOnly == 1) {
1454 $html = $this->cObj->substituteSubpart($html, '###ROW_LONG###', '');
1455 } elseif ($headerOnly == 2) {
1456 $html = $this->cObj->substituteSubpart($html, '###ROW_SHORT###', '');
1457 $html = $this->cObj->substituteSubpart($html, '###ROW_LONG###', '');
1458 }
1459 if (is_array($tmplContent)) {
1460 foreach ($tmplContent as $k => $v) {
1461 $markerArray['###' . GeneralUtility::strtoupper($k) . '###'] = $v;
1462 }
1463 }
1464 // Description text
1465 $markerArray['###TEXT_ITEM_SIZE###'] = $this->pi_getLL('res_size', '', TRUE);
1466 $markerArray['###TEXT_ITEM_CRDATE###'] = $this->pi_getLL('res_created', '', TRUE);
1467 $markerArray['###TEXT_ITEM_MTIME###'] = $this->pi_getLL('res_modified', '', TRUE);
1468 $markerArray['###TEXT_ITEM_PATH###'] = $this->pi_getLL('res_path', '', TRUE);
1469 $html = $this->cObj->substituteMarkerArrayCached($html, $markerArray, array(), array());
1470 // If there are subrows (eg. subpages in a PDF-file or if a duplicate page is selected due to user-login (phash_grouping))
1471 if (is_array($row['_sub'])) {
1472 if ($this->multiplePagesType($row['item_type'])) {
1473 $html = str_replace('###TEXT_ROW_SUB###', $this->pi_getLL('res_otherMatching', '', TRUE), $html);
1474 foreach ($row['_sub'] as $subRow) {
1475 $html .= $this->printResultRow($subRow, 1);
1476 }
1477 } else {
1478 $markerArray['###TEXT_ROW_SUB###'] = $this->pi_getLL('res_otherMatching', '', TRUE);
1479 $html = str_replace('###TEXT_ROW_SUB###', $this->pi_getLL('res_otherPageAsWell', '', TRUE), $html);
1480 }
1481 }
1482 return $html;
1483 }
1484 }
1485
1486 /**
1487 * Returns a results browser
1488 *
1489 * @param bool $showResultCount Show result count
1490 * @param string $addString String appended to "displaying results..." notice.
1491 * @param string $addPart String appended after section "displaying results...
1492 * @param string $freeIndexUid List of integers pointing to free indexing configurations to search. -1 represents no filtering, 0 represents TYPO3 pages only, any number above zero is a uid of an indexing configuration!
1493 * @return string HTML output
1494 */
1495 public function pi_list_browseresults($showResultCount = TRUE, $addString = '', $addPart = '', $freeIndexUid = -1) {
1496 // Initializing variables:
1497 $pointer = (int)$this->piVars['pointer'];
1498 $count = (int)$this->internal['res_count'];
1499 $results_at_a_time = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($this->internal['results_at_a_time'], 1, 1000);
1500 $pageCount = (int)ceil($count / $results_at_a_time);
1501
1502 $links = array();
1503 // only show the result browser if more than one page is needed
1504 if ($pageCount > 1) {
1505 $maxPages = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($this->internal['maxPages'], 1, $pageCount);
1506
1507 // Make browse-table/links:
1508 if ($pointer > 0) {
1509 // all pages after the 1st one
1510 $links[] = '<li>' . $this->makePointerSelector_link($this->pi_getLL('pi_list_browseresults_prev', '< Previous', TRUE), $pointer - 1, $freeIndexUid) . '</li>';
1511 }
1512 $minPage = $pointer - (int)floor($maxPages / 2);
1513 $maxPage = $minPage + $maxPages - 1;
1514 // Check if the indexes are within the page limits
1515 if ($minPage < 0) {
1516 $maxPage -= $minPage;
1517 $minPage = 0;
1518 } elseif ($maxPage >= $pageCount) {
1519 $minPage -= $maxPage - $pageCount + 1;
1520 $maxPage = $pageCount - 1;
1521 }
1522 $pageLabel = $this->pi_getLL('pi_list_browseresults_page', 'Page', TRUE);
1523 for ($a = $minPage; $a <= $maxPage; $a++) {
1524 $label = trim($pageLabel . ' ' . ($a + 1));
1525 $link = $this->makePointerSelector_link($label, $a, $freeIndexUid);
1526 if ($a === $pointer) {
1527 $links[] = '<li' . $this->pi_classParam('browselist-currentPage') . '><strong>' . $link . '</strong></li>';
1528 } else {
1529 $links[] = '<li>' . $link . '</li>';
1530 }
1531 }
1532 if ($pointer + 1 < $pageCount) {
1533 $links[] = '<li>' . $this->makePointerSelector_link($this->pi_getLL('pi_list_browseresults_next', 'Next >', TRUE), $pointer + 1, $freeIndexUid) . '</li>';
1534 }
1535 }
1536 if (!empty($links)) {
1537 $addPart .= '
1538 <ul class="browsebox">
1539 ' . implode('', $links) . '
1540 </ul>';
1541 }
1542 $label = str_replace(
1543 array('###TAG_BEGIN###', '###TAG_END###'),
1544 array('<strong>', '</strong>'),
1545 $this->pi_getLL('pi_list_browseresults_display', 'Displaying results ###TAG_BEGIN###%1$s to %2$s###TAG_END### out of ###TAG_BEGIN###%3$s###TAG_END###')
1546 );
1547 $resultsFrom = $pointer * $results_at_a_time + 1;
1548 $resultsTo = min($resultsFrom + $results_at_a_time - 1, $count);
1549 $resultCountText = '';
1550 if ($showResultCount) {
1551 $resultCountText = '<p>' . sprintf($label, $resultsFrom, $resultsTo, $count) . $addString . '</p>';
1552 }
1553 $sTables = '<div' . $this->pi_classParam('browsebox') . '>'
1554 . $resultCountText
1555 . $addPart . '</div>';
1556 return $sTables;
1557 }
1558
1559 /***********************************
1560 *
1561 * Support functions for HTML output (with a minimum of fixed markup)
1562 *
1563 ***********************************/
1564 /**
1565 * Preparing template data for the result row output
1566 *
1567 * @param array Result row
1568 * @param bool If set, display only header of result (for sub-results)
1569 * @return array Array with data to insert in result row template
1570 */
1571 public function prepareResultRowTemplateData($row, $headerOnly) {
1572 // Initialize:
1573 $specRowConf = $this->getSpecialConfigForRow($row);
1574 $CSSsuffix = $specRowConf['CSSsuffix'] ? '-' . $specRowConf['CSSsuffix'] : '';
1575 // If external media, link to the media-file instead.
1576 if ($row['item_type']) {
1577 // External media
1578 if ($row['show_resume']) {
1579 // Can link directly.
1580 $targetAttribute = '';
1581 if ($GLOBALS['TSFE']->config['config']['fileTarget']) {
1582 $targetAttribute = ' target="' . htmlspecialchars($GLOBALS['TSFE']->config['config']['fileTarget']) . '"';
1583 }
1584 $title = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($this->makeTitle($row)) . '</a>';
1585 } else {
1586 // Suspicious, so linking to page instead...
1587 $copy_row = $row;
1588 unset($copy_row['cHashParams']);
1589 $title = $this->linkPage($row['page_id'], htmlspecialchars($this->makeTitle($row)), $copy_row);
1590 }
1591 } else {
1592 // Else the page:
1593 // Prepare search words for markup in content:
1594 if ($this->conf['forwardSearchWordsInResultLink']) {
1595 $markUpSwParams = array('no_cache' => 1);
1596 foreach ($this->sWArr as $d) {
1597 $markUpSwParams['sword_list'][] = $d['sword'];
1598 }
1599 } else {
1600 $markUpSwParams = array();
1601 }
1602 $title = $this->linkPage($row['data_page_id'], htmlspecialchars($this->makeTitle($row)), $row, $markUpSwParams);
1603 }
1604 $tmplContent = array();
1605 $tmplContent['title'] = $title;
1606 $tmplContent['result_number'] = $this->conf['show.']['resultNumber'] ? $row['result_number'] . ': ' : '&nbsp;';
1607 $tmplContent['icon'] = $this->makeItemTypeIcon($row['item_type'], '', $specRowConf);
1608 $tmplContent['rating'] = $this->makeRating($row);
1609 $tmplContent['description'] = $this->makeDescription($row, $this->piVars['extResume'] && !$headerOnly ? 0 : 1);
1610 $tmplContent = $this->makeInfo($row, $tmplContent);
1611 $tmplContent['access'] = $this->makeAccessIndication($row['page_id']);
1612 $tmplContent['language'] = $this->makeLanguageIndication($row);
1613 $tmplContent['CSSsuffix'] = $CSSsuffix;
1614 // Post processing with hook.
1615 if ($hookObj = $this->hookRequest('prepareResultRowTemplateData_postProc')) {
1616 $tmplContent = $hookObj->prepareResultRowTemplateData_postProc($tmplContent, $row, $headerOnly);
1617 }
1618 return $tmplContent;
1619 }
1620
1621 /**
1622 * Returns a string that tells which search words are searched for.
1623 *
1624 * @param array Array of search words
1625 * @return string HTML telling what is searched for.
1626 */
1627 public function tellUsWhatIsSeachedFor($sWArr) {
1628 // Init:
1629 $searchingFor = '';
1630 $c = 0;
1631 // Traverse search words:
1632 foreach ($sWArr as $k => $v) {
1633 if ($c) {
1634 switch ($v['oper']) {
1635 case 'OR':
1636 $searchingFor .= ' ' . $this->pi_getLL('searchFor_or', '', TRUE) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1637 break;
1638 case 'AND NOT':
1639 $searchingFor .= ' ' . $this->pi_getLL('searchFor_butNot', '', TRUE) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1640 break;
1641 default:
1642 // AND...
1643 $searchingFor .= ' ' . $this->pi_getLL('searchFor_and', '', TRUE) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1644 }
1645 } else {
1646 $searchingFor = $this->pi_getLL('searchFor', '', TRUE) . ' ' . $this->wrapSW($this->utf8_to_currentCharset($v['sword']));
1647 }
1648 $c++;
1649 }
1650 return $searchingFor;
1651 }
1652
1653 /**
1654 * Wraps the search words in the search-word list display (from ->tellUsWhatIsSeachedFor())
1655 *
1656 * @param string search word to wrap (in local charset!)
1657 * @return string Search word wrapped in <span> tag.
1658 */
1659 public function wrapSW($str) {
1660 return '"<span' . $this->pi_classParam('sw') . '>' . htmlspecialchars($str) . '</span>"';
1661 }
1662
1663 /**
1664 * Makes a selector box
1665 *
1666 * @param string Name of selector box
1667 * @param string Current value
1668 * @param array Array of options in the selector box (value => label pairs)
1669 * @return string HTML of selector box
1670 */
1671 public function renderSelectBox($name, $value, $optValues) {
1672 if (is_array($optValues)) {
1673 $opt = array();
1674 $isSelFlag = 0;
1675 foreach ($optValues as $k => $v) {
1676 $sel = (string)$k === (string)$value ? ' selected="selected"' : '';
1677 if ($sel) {
1678 $isSelFlag++;
1679 }
1680 $opt[] = '<option value="' . htmlspecialchars($k) . '"' . $sel . '>' . htmlspecialchars($v) . '</option>';
1681 }
1682 return '<select name="' . $name . '">' . implode('', $opt) . '</select>';
1683 }
1684 }
1685
1686 /**
1687 * Used to make the link for the result-browser.
1688 * Notice how the links must resubmit the form after setting the new pointer-value in a hidden formfield.
1689 *
1690 * @param string String to wrap in <a> tag
1691 * @param int Pointer value
1692 * @param string List of integers pointing to free indexing configurations to search. -1 represents no filtering, 0 represents TYPO3 pages only, any number above zero is a uid of an indexing configuration!
1693 * @return string Input string wrapped in <a> tag with onclick event attribute set.
1694 */
1695 public function makePointerSelector_link($str, $p, $freeIndexUid) {
1696 $onclick = 'document.getElementById(\'' . $this->prefixId . '_pointer\').value=\'' . $p . '\';' . 'document.getElementById(\'' . $this->prefixId . '_freeIndexUid\').value=\'' . rawurlencode($freeIndexUid) . '\';' . 'document.getElementById(\'' . $this->prefixId . '\').submit();return false;';
1697 return '<a href="#" onclick="' . htmlspecialchars($onclick) . '">' . $str . '</a>';
1698 }
1699
1700 /**
1701 * Return icon for file extension
1702 *
1703 * @param string File extension / item type
1704 * @param string Title attribute value in icon.
1705 * @param array TypoScript configuration specifically for search result.
1706 * @return string <img> tag for icon
1707 */
1708 public function makeItemTypeIcon($it, $alt = '', $specRowConf) {
1709 // Build compound key if item type is 0, iconRendering is not used
1710 // and specConfs.[pid].pageIcon was set in TS
1711 if ($it === '0' && $specRowConf['_pid'] && is_array($specRowConf['pageIcon.']) && !is_array($this->conf['iconRendering.'])) {
1712 $it .= ':' . $specRowConf['_pid'];
1713 }
1714 if (!isset($this->iconFileNameCache[$it])) {
1715 $this->iconFileNameCache[$it] = '';
1716 // If TypoScript is used to render the icon:
1717 if (is_array($this->conf['iconRendering.'])) {
1718 $this->cObj->setCurrentVal($it);
1719 $this->iconFileNameCache[$it] = $this->cObj->cObjGetSingle($this->conf['iconRendering'], $this->conf['iconRendering.']);
1720 } else {
1721 // Default creation / finding of icon:
1722 $icon = '';
1723 if ($it === '0' || substr($it, 0, 2) == '0:') {
1724 if (is_array($specRowConf['pageIcon.'])) {
1725 $this->iconFileNameCache[$it] = $this->cObj->IMAGE($specRowConf['pageIcon.']);
1726 } else {
1727 $icon = 'EXT:indexed_search/pi/res/pages.gif';
1728 }
1729 } elseif ($this->external_parsers[$it]) {
1730 $icon = $this->external_parsers[$it]->getIcon($it);
1731 }
1732 if ($icon) {
1733 $fullPath = GeneralUtility::getFileAbsFileName($icon);
1734 if ($fullPath) {
1735 $info = @getimagesize($fullPath);
1736 $iconPath = \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix($fullPath);
1737 $this->iconFileNameCache[$it] = is_array($info) ? '<img src="' . $iconPath . '" ' . $info[3] . ' title="' . htmlspecialchars($alt) . '" alt="" />' : '';
1738 }
1739 }
1740 }
1741 }
1742 return $this->iconFileNameCache[$it];
1743 }
1744
1745 /**
1746 * Return the rating-HTML code for the result row. This makes use of the $this->firstRow
1747 *
1748 * @param array Result row array
1749 * @return string String showing ranking value
1750 */
1751 public function makeRating($row) {
1752 switch ((string)$this->piVars['order']) {
1753 case 'rank_count':
1754 // Number of occurencies on page
1755 return $row['order_val'] . ' ' . $this->pi_getLL('maketitle_matches');
1756 break;
1757 case 'rank_first':
1758 // Close to top of page
1759 return ceil(\TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange((255 - $row['order_val']), 1, 255) / 255 * 100) . '%';
1760 break;
1761 case 'rank_flag':
1762 // Based on priority assigned to <title> / <meta-keywords> / <meta-description> / <body>
1763 if ($this->firstRow['order_val2']) {
1764 $base = $row['order_val1'] * 256;
1765 // (3 MSB bit, 224 is highest value of order_val1 currently)
1766 $freqNumber = $row['order_val2'] / $this->firstRow['order_val2'] * pow(2, 12);
1767 // 15-3 MSB = 12
1768 $total = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($base + $freqNumber, 0, 32767);
1769 return ceil(log($total) / log(32767) * 100) . '%';
1770 }
1771 break;
1772 case 'rank_freq':
1773 // Based on frequency
1774 $max = 10000;
1775 $total = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($row['order_val'], 0, $max);
1776 return ceil(log($total) / log($max) * 100) . '%';
1777 break;
1778 case 'crdate':
1779 // Based on creation date
1780 return $this->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_crdate'], 0);
1781 break;
1782 case 'mtime':
1783 // Based on modification time
1784 return $this->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_mtime'], 0);
1785 break;
1786 default:
1787 // fx. title
1788 return '&nbsp;';
1789 }
1790 }
1791
1792 /**
1793 * Returns the resume for the search-result.
1794 *
1795 * @param array Search result row
1796 * @param bool 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.
1797 * @param int String length
1798 * @return string HTML string ...
1799 */
1800 public function makeDescription($row, $noMarkup = 0, $lgd = 180) {
1801 if ($row['show_resume']) {
1802 if (!$noMarkup) {
1803 $markedSW = '';
1804 if (\TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility::isTableUsed('index_fulltext')) {
1805 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'index_fulltext', 'phash=' . (int)$row['phash']);
1806 } else {
1807 $res = FALSE;
1808 }
1809 if ($res) {
1810 if ($ftdrow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
1811 // Cut HTTP references after some length
1812 $content = preg_replace('/(http:\\/\\/[^ ]{60})([^ ]+)/i', '$1...', $ftdrow['fulltextdata']);
1813 $markedSW = $this->markupSWpartsOfString($content);
1814 }
1815 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1816 }
1817 }
1818 if (!trim($markedSW)) {
1819 $outputStr = $GLOBALS['TSFE']->csConvObj->crop('utf-8', $row['item_description'], $lgd);
1820 $outputStr = htmlspecialchars($outputStr);
1821 }
1822 $output = $this->utf8_to_currentCharset($outputStr ?: $markedSW);
1823 } else {
1824 $output = '<span class="noResume">' . $this->pi_getLL('res_noResume', '', TRUE) . '</span>';
1825 }
1826 return $output;
1827 }
1828
1829 /**
1830 * Marks up the search words from $this->sWarr in the $str with a color.
1831 *
1832 * @param string Text in which to find and mark up search words. This text is assumed to be UTF-8 like the search words internally is.
1833 * @return string Processed content.
1834 */
1835 public function markupSWpartsOfString($str) {
1836 // Init:
1837 $str = str_replace('&nbsp;', ' ', \TYPO3\CMS\Core\Html\HtmlParser::bidir_htmlspecialchars($str, -1));
1838 $str = preg_replace('/\\s\\s+/', ' ', $str);
1839 $swForReg = array();
1840 // Prepare search words for regex:
1841 foreach ($this->sWArr as $d) {
1842 $swForReg[] = preg_quote($d['sword'], '/');
1843 }
1844 $regExString = '(' . implode('|', $swForReg) . ')';
1845 // Split and combine:
1846 $parts = preg_split('/' . $regExString . '/ui', ' ' . $str . ' ', 20000, PREG_SPLIT_DELIM_CAPTURE);
1847 // Constants:
1848 $summaryMax = 300;
1849 $postPreLgd = 60;
1850 $postPreLgd_offset = 5;
1851 $divider = ' ... ';
1852 $occurencies = (count($parts) - 1) / 2;
1853 if ($occurencies) {
1854 $postPreLgd = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($summaryMax / $occurencies, $postPreLgd, $summaryMax / 2);
1855 }
1856 // Variable:
1857 $summaryLgd = 0;
1858 $output = array();
1859 // Shorten in-between strings:
1860 foreach ($parts as $k => $strP) {
1861 if ($k % 2 == 0) {
1862 // Find length of the summary part:
1863 $strLen = $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $parts[$k]);
1864 $output[$k] = $parts[$k];
1865 // Possibly shorten string:
1866 if (!$k) {
1867 // First entry at all (only cropped on the frontside)
1868 if ($strLen > $postPreLgd) {
1869 $output[$k] = $divider . preg_replace('/^[^[:space:]]+[[:space:]]/', '', $GLOBALS['TSFE']->csConvObj->crop('utf-8', $parts[$k], -($postPreLgd - $postPreLgd_offset)));
1870 }
1871 } elseif ($summaryLgd > $summaryMax || !isset($parts[($k + 1)])) {
1872 // In case summary length is exceed OR if there are no more entries at all:
1873 if ($strLen > $postPreLgd) {
1874 $output[$k] = preg_replace('/[[:space:]][^[:space:]]+$/', '', $GLOBALS['TSFE']->csConvObj->crop('utf-8', $parts[$k], ($postPreLgd - $postPreLgd_offset))) . $divider;
1875 }
1876 } else {
1877 // In-between search words:
1878 if ($strLen > $postPreLgd * 2) {
1879 $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)));
1880 }
1881 }
1882 $summaryLgd += $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $output[$k]);
1883 // Protect output:
1884 $output[$k] = htmlspecialchars($output[$k]);
1885 // If summary lgd is exceed, break the process:
1886 if ($summaryLgd > $summaryMax) {
1887 break;
1888 }
1889 } else {
1890 $summaryLgd += $GLOBALS['TSFE']->csConvObj->strlen('utf-8', $strP);
1891 $output[$k] = '<strong class="tx-indexedsearch-redMarkup">' . htmlspecialchars($parts[$k]) . '</strong>';
1892 }
1893 }
1894 // Return result:
1895 return implode('', $output);
1896 }
1897
1898 /**
1899 * Returns the title of the search result row
1900 *
1901 * @param array Result row
1902 * @return string Title from row
1903 */
1904 public function makeTitle($row) {
1905 $add = '';
1906 if ($this->multiplePagesType($row['item_type'])) {
1907 $dat = unserialize($row['cHashParams']);
1908 $pp = explode('-', $dat['key']);
1909 if ($pp[0] != $pp[1]) {
1910 $add = ', ' . $this->pi_getLL('word_pages') . ' ' . $dat['key'];
1911 } else {
1912 $add = ', ' . $this->pi_getLL('word_page') . ' ' . $pp[0];
1913 }
1914 }
1915 $outputString = $GLOBALS['TSFE']->csConvObj->crop('utf-8', $row['item_title'], 50, '...');
1916 return $this->utf8_to_currentCharset($outputString) . $add;
1917 }
1918
1919 /**
1920 * Returns the info-string in the bottom of the result-row display (size, dates, path)
1921 *
1922 * @param array Result row
1923 * @param array Template array to modify
1924 * @return array Modified template array
1925 */
1926 public function makeInfo($row, $tmplArray) {
1927 $tmplArray['size'] = GeneralUtility::formatSize($row['item_size']);
1928 $tmplArray['created'] = $this->formatCreatedDate($row['item_crdate']);
1929 $tmplArray['modified'] = $this->formatModifiedDate($row['item_mtime']);
1930 $pathId = $row['data_page_id'] ?: $row['page_id'];
1931 $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
1932 $pI = parse_url($row['data_filename']);
1933 if ($pI['scheme']) {
1934 $targetAttribute = '';
1935 if ($GLOBALS['TSFE']->config['config']['fileTarget']) {
1936 $targetAttribute = ' target="' . htmlspecialchars($GLOBALS['TSFE']->config['config']['fileTarget']) . '"';
1937 }
1938 $tmplArray['path'] = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($row['data_filename']) . '</a>';
1939 } else {
1940 $pathStr = htmlspecialchars($this->getPathFromPageId($pathId, $pathMP));
1941 $tmplArray['path'] = $this->linkPage($pathId, $pathStr, array(
1942 'cHashParams' => $row['cHashParams'],
1943 'data_page_type' => $row['data_page_type'],
1944 'data_page_mp' => $pathMP,
1945 'sys_language_uid' => $row['sys_language_uid']
1946 ));
1947 }
1948 return $tmplArray;
1949 }
1950
1951 /**
1952 * Returns configuration from TypoScript for result row based on ID / location in page tree!
1953 *
1954 * @param array Result row
1955 * @return array Configuration array
1956 */
1957 public function getSpecialConfigForRow($row) {
1958 $pathId = $row['data_page_id'] ?: $row['page_id'];
1959 $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
1960 $rl = $this->getRootLine($pathId, $pathMP);
1961 $specConf = $this->conf['specConfs.']['0.'];
1962 if (is_array($rl)) {
1963 foreach ($rl as $dat) {
1964 if (is_array($this->conf['specConfs.'][$dat['uid'] . '.'])) {
1965 $specConf = $this->conf['specConfs.'][$dat['uid'] . '.'];
1966 $specConf['_pid'] = $dat['uid'];
1967 break;
1968 }
1969 }
1970 }
1971 return $specConf;
1972 }
1973
1974 /**
1975 * Returns the HTML code for language indication.
1976 *
1977 * @param array Result row
1978 * @return string HTML code for result row.
1979 */
1980 public function makeLanguageIndication($row) {
1981 // If search result is a TYPO3 page:
1982 if ((string)$row['item_type'] === '0') {
1983 // If TypoScript is used to render the flag:
1984 if (is_array($this->conf['flagRendering.'])) {
1985 $this->cObj->setCurrentVal($row['sys_language_uid']);
1986 return $this->cObj->cObjGetSingle($this->conf['flagRendering'], $this->conf['flagRendering.']);
1987 }
1988 }
1989 return '&nbsp;';
1990 }
1991
1992 /**
1993 * Returns the HTML code for the locking symbol.
1994 * NOTICE: Requires a call to ->getPathFromPageId() first in order to work (done in ->makeInfo() by calling that first)
1995 *
1996 * @param int Page id for which to find answer
1997 * @return string <img> tag if access is limited.
1998 */
1999 public function makeAccessIndication($id) {
2000 if (is_array($this->fe_groups_required[$id]) && count($this->fe_groups_required[$id])) {
2001 return '<img src="' . \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::siteRelPath('indexed_search') . 'pi/res/locked.gif" width="12" height="15" vspace="5" title="' . sprintf($this->pi_getLL('res_memberGroups', '', TRUE), implode(',', array_unique($this->fe_groups_required[$id]))) . '" alt="" />';
2002 }
2003 }
2004
2005 /**
2006 * Links the $str to page $id
2007 *
2008 * @param int Page id
2009 * @param string Title String to link
2010 * @param array Result row
2011 * @param array Additional parameters for marking up seach words
2012 * @return string <A> tag wrapped title string.
2013 */
2014 public function linkPage($id, $str, $row = array(), $markUpSwParams = array()) {
2015 // Parameters for link:
2016 $urlParameters = (array)unserialize($row['cHashParams']);
2017 // Add &type and &MP variable:
2018 if ($row['data_page_type']) {
2019 $urlParameters['type'] = $row['data_page_type'];
2020 }
2021 if ($row['data_page_mp']) {
2022 $urlParameters['MP'] = $row['data_page_mp'];
2023 }
2024 if ($row['sys_language_uid']) {
2025 $urlParameters['L'] = $row['sys_language_uid'];
2026 }
2027 // markup-GET vars:
2028 $urlParameters = array_merge($urlParameters, $markUpSwParams);
2029 // This will make sure that the path is retrieved if it hasn't been already. Used only for the sake of the domain_record thing...
2030 if (!is_array($this->domain_records[$id])) {
2031 $this->getPathFromPageId($id);
2032 }
2033 // If external domain, then link to that:
2034 if (count($this->domain_records[$id])) {
2035 reset($this->domain_records[$id]);
2036 $firstDom = current($this->domain_records[$id]);
2037 $scheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://';
2038 $addParams = '';
2039 if (is_array($urlParameters)) {
2040 if (count($urlParameters)) {
2041 $addParams .= GeneralUtility::implodeArrayForUrl('', $urlParameters);
2042 }
2043 }
2044 if ($target = $this->conf['search.']['detect_sys_domain_records.']['target']) {
2045 $target = ' target="' . $target . '"';
2046 }
2047 return '<a href="' . htmlspecialchars(($scheme . $firstDom . '/index.php?id=' . $id . $addParams)) . '"' . $target . '>' . htmlspecialchars($str) . '</a>';
2048 } else {
2049 return $this->pi_linkToPage($str, $id, $this->conf['result_link_target'], $urlParameters);
2050 }
2051 }
2052
2053 /**
2054 * Returns the path to the page $id
2055 *
2056 * @param int Page ID
2057 * @param string MP variable content.
2058 * @return string Root line for result.
2059 */
2060 public function getRootLine($id, $pathMP = '') {
2061 $identStr = $id . '|' . $pathMP;
2062 if (!isset($this->cache_path[$identStr])) {
2063 $this->cache_rl[$identStr] = $GLOBALS['TSFE']->sys_page->getRootLine($id, $pathMP);
2064 }
2065 return $this->cache_rl[$identStr];
2066 }
2067
2068 /**
2069 * Gets the first sys_domain record for the page, $id
2070 *
2071 * @param int Page id
2072 * @return string Domain name
2073 */
2074 public function getFirstSysDomainRecordForPage($id) {
2075 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('domainName', 'sys_domain', 'pid=' . (int)$id . $this->cObj->enableFields('sys_domain'), '', 'sorting');
2076 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
2077 return rtrim($row['domainName'], '/');
2078 }
2079
2080 /**
2081 * Returns the path to the page $id
2082 *
2083 * @param int Page ID
2084 * @param string MP variable content
2085 * @return string Path
2086 */
2087 public function getPathFromPageId($id, $pathMP = '') {
2088 $identStr = $id . '|' . $pathMP;
2089 if (!isset($this->cache_path[$identStr])) {
2090 $this->fe_groups_required[$id] = array();
2091 $this->domain_records[$id] = array();
2092 $rl = $this->getRootLine($id, $pathMP);
2093 $hitRoot = 0;
2094 $path = '';
2095 if (is_array($rl) && count($rl)) {
2096 foreach ($rl as $k => $v) {
2097 // Check fe_user
2098 if ($v['fe_group'] && ($v['uid'] == $id || $v['extendToSubpages'])) {
2099 $this->fe_groups_required[$id][] = $v['fe_group'];
2100 }
2101 // Check sys_domain.
2102 if ($this->conf['search.']['detect_sys_domain_records']) {
2103 $sysDName = $this->getFirstSysDomainRecordForPage($v['uid']);
2104 if ($sysDName) {
2105 $this->domain_records[$id][] = $sysDName;
2106 // Set path accordingly:
2107 $path = $sysDName . $path;
2108 break;
2109 }
2110 }
2111 // Stop, if we find that the current id is the current root page.
2112 if ($v['uid'] == $GLOBALS['TSFE']->config['rootLine'][0]['uid']) {
2113 break;
2114 }
2115 $path = '/' . $v['title'] . $path;
2116 }
2117 }
2118 $this->cache_path[$identStr] = $path;
2119 if (is_array($this->conf['path_stdWrap.'])) {
2120 $this->cache_path[$identStr] = $this->cObj->stdWrap($this->cache_path[$identStr], $this->conf['path_stdWrap.']);
2121 }
2122 }
2123 return $this->cache_path[$identStr];
2124 }
2125
2126 /**
2127 * Return the menu of pages used for the selector.
2128 *
2129 * @param int Page ID for which to return menu
2130 * @return array Menu items (for making the section selector box)
2131 */
2132 public function getMenu($id) {
2133 if ($this->conf['show.']['LxALLtypes']) {
2134 $output = array();
2135 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('title,uid', 'pages', 'pid=' . (int)$id . $this->cObj->enableFields('pages'), '', 'sorting');
2136 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
2137 $output[$row['uid']] = $GLOBALS['TSFE']->sys_page->getPageOverlay($row);
2138 }
2139 $GLOBALS['TYPO3_DB']->sql_free_result($res);
2140 return $output;
2141 } else {
2142 return $GLOBALS['TSFE']->sys_page->getMenu($id);
2143 }
2144 }
2145
2146 /**
2147 * Returns if an item type is a multipage item type
2148 *
2149 * @param string Item type
2150 * @return bool TRUE if multipage capable
2151 */
2152 public function multiplePagesType($item_type) {
2153 return is_object($this->external_parsers[$item_type]) && $this->external_parsers[$item_type]->isMultiplePageExtension($item_type);
2154 }
2155
2156 /**
2157 * Converts the input string from utf-8 to the backend charset.
2158 *
2159 * @param string String to convert (utf-8)
2160 * @return string Converted string (backend charset if different from utf-8)
2161 */
2162 public function utf8_to_currentCharset($str) {
2163 return $GLOBALS['TSFE']->csConv($str, 'utf-8');
2164 }
2165
2166 /**
2167 * Returns an object reference to the hook object if any
2168 *
2169 * @param string Name of the function you want to call / hook key
2170 * @return object Hook object, if any. Otherwise NULL.
2171 */
2172 public function hookRequest($functionName) {
2173 // Hook: menuConfig_preProcessModMenu
2174 if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
2175 $hookObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
2176 if (method_exists($hookObj, $functionName)) {
2177 $hookObj->pObj = $this;
2178 return $hookObj;
2179 }
2180 }
2181 }
2182
2183 /**
2184 * Obtains the URL of the search target page
2185 *
2186 * @return string
2187 */
2188 protected function getSearchFormActionURL() {
2189 $targetUrlPid = $this->getSearchFormActionPidFromTS();
2190 if ($targetUrlPid == 0) {
2191 $targetUrlPid = $GLOBALS['TSFE']->id;
2192 }
2193 return $this->pi_getPageLink($targetUrlPid, $GLOBALS['TSFE']->sPre);
2194 }
2195
2196 /**
2197 * Obtains search form target pid from the TypoScript configuration
2198 *
2199 * @return int
2200 */
2201 protected function getSearchFormActionPidFromTS() {
2202 $result = 0;
2203 if (isset($this->conf['search.']['targetPid']) || isset($this->conf['search.']['targetPid.'])) {
2204 if (is_array($this->conf['search.']['targetPid.'])) {
2205 $result = $this->cObj->stdWrap($this->conf['search.']['targetPid'], $this->conf['search.']['targetPid.']);
2206 } else {
2207 $result = $this->conf['search.']['targetPid'];
2208 }
2209 $result = (int)$result;
2210 }
2211 return $result;
2212 }
2213
2214 /**
2215 * Formats date as 'created' date
2216 *
2217 * @param int $date
2218 * @return string
2219 */
2220 protected function formatCreatedDate($date) {
2221 $defaultFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
2222 return $this->formatDate($date, 'created', $defaultFormat);
2223 }
2224
2225 /**
2226 * Formats date as 'modified' date
2227 *
2228 * @param int $date
2229 * @return string
2230 */
2231 protected function formatModifiedDate($date) {
2232 $defaultFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
2233 return $this->formatDate($date, 'modified', $defaultFormat);
2234 }
2235
2236 /**
2237 * Formats the date using format string from TypoScript or default format
2238 * if TypoScript format is not set
2239 *
2240 * @param int $date
2241 * @param string $tsKey
2242 * @param string $defaultFormat
2243 * @return string
2244 */
2245 protected function formatDate($date, $tsKey, $defaultFormat) {
2246 $strftimeFormat = $this->conf['dateFormat.'][$tsKey];
2247 if ($strftimeFormat) {
2248 $result = strftime($strftimeFormat, $date);
2249 } else {
2250 $result = date($defaultFormat, $date);
2251 }
2252 return $result;
2253 }
2254
2255 }