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