PageRenderTime 71ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/typo3/sysext/indexed_search/pi/class.tx_indexedsearch.php

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