2 /***************************************************************
5 * (c) 1999-2009 Kasper Skaarhoj (kasperYYYY@typo3.com)
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.
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.
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.
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
28 * Searching in database tables, typ. "pages" and "tt_content"
29 * Used to generate search queries for TypoScript.
30 * The class is included from "class.tslib_pagegen.php" based on whether there has been detected content in the GPvar "sword"
33 * Revised for TYPO3 3.6 June/2003 by Kasper Skaarhoj
35 * @author Kasper Skaarhoj <kasperYYYY@typo3.com>
36 * @author Rene Fritz <r.fritz@colorcube.de>
39 * [CLASS/FUNCTION INDEX of SCRIPT]
43 * 88: class tslib_search
44 * 127: function register_tables_and_columns($requestedCols,$allowedCols)
45 * 168: function explodeCols($in)
46 * 193: function register_and_explode_search_string($sword)
47 * 226: function split($origSword, $specchars='+-', $delchars='+.,-')
48 * 269: function quotemeta($str)
49 * 285: function build_search_query($endClause)
50 * 371: function build_search_query_for_searchwords()
51 * 413: function get_operator($operator)
52 * 436: function count_query()
53 * 449: function execute_query()
54 * 462: function get_searchwords()
55 * 477: function get_searchwordsArray()
58 * (This index is automatically created/updated by the extension "extdeveval")
81 * Search class used for the content object SEARCHRESULT
83 * @author Kasper Skaarhoj <kasperYYYY@typo3.com>
86 * @see tslib_cObj::SEARCHRESULT()
89 var $tables = Array ();
91 var $group_by = 'PRIMARY_KEY'; // Alternatively 'PRIMARY_KEY'; sorting by primary key
92 var $default_operator = 'AND'; // Standard SQL-operator between words
93 var $operator_translate_table_caseinsensitive = TRUE;
94 var $operator_translate_table = Array ( // case-sensitiv. Defineres the words, which will be operators between words
97 Array ('-' , 'AND NOT'),
99 Array ('and' , 'AND'),
101 Array ('not' , 'AND NOT'),
105 var $sword_array; // Contains the search-words and operators
106 var $queryParts; // Contains the query parts after processing.
108 var $other_where_clauses; // Addition to the whereclause. This could be used to limit search to a certain page or alike in the system.
109 var $fTable; // This is set with the foreign table that 'pages' are connected to.
111 var $res_offset = 0; // How many rows to offset from the beginning
112 var $res_shows = 20; // How many results to show (0 = no limit)
113 var $res_count; // Intern: How many results, there was last time (with the exact same searchstring.
115 var $pageIdList=''; // List of pageIds.
117 var $listOfSearchFields ='';
120 * Creates the $this->tables-array.
121 * The 'pages'-table is ALWAYS included as the search is page-based. Apart from this there may be one and only one table, joined with the pages-table. This table is the first table mentioned in the requested-list. If any more tables are set here, they are ignored.
123 * @param string is a list (-) of columns that we want to search. This could be input from the search-form (see TypoScript documentation)
124 * @param string $allowedCols: is the list of columns, that MAY be searched. All allowed cols are set as result-fields. All requested cols MUST be in the allowed-fields list.
127 function register_tables_and_columns($requestedCols,$allowedCols) {
128 $rCols=$this->explodeCols($requestedCols);
129 $aCols=$this->explodeCols($allowedCols);
131 foreach ($rCols as $k => $v) {
133 if (in_array($rCols[$k], $aCols)) {
134 $parts = explode('.',$rCols[$k]);
135 $this->tables
[$parts[0]]['searchfields'][] = $parts[1];
138 $this->tables
['pages']['primary_key'] = 'uid';
139 $this->tables
['pages']['resultfields'][] = 'uid';
140 unset($this->tables
['pages']['fkey']);
142 foreach ($aCols as $k => $v) {
144 $parts = explode('.',$aCols[$k]);
145 $this->tables
[$parts[0]]['resultfields'][] = $parts[1].' AS '.str_replace('.','_',$aCols[$k]);
146 $this->tables
[$parts[0]]['fkey']='pid';
150 foreach ($this->tables
as $t => $v) {
152 if (!$this->fTable
) {
155 unset($this->tables
[$t]);
162 * Function that can convert the syntax for entering which tables/fields the search should be conducted in.
164 * @param string This is the code-line defining the tables/fields to search. Syntax: '[table1].[field1]-[field2]-[field3] : [table2].[field1]-[field2]'
165 * @return array An array where the values is "[table].[field]" strings to search
166 * @see register_tables_and_columns()
168 function explodeCols($in) {
169 $theArray = explode(':',$in);
171 foreach ($theArray as $val) {
173 $parts = explode('.',$val);
174 if ($parts[0] && $parts[1]) {
175 $subparts = explode('-',$parts[1]);
176 foreach ($subparts as $piece) {
178 if ($piece) $out[]=$parts[0].'.'.$piece;
186 * Takes a search-string (WITHOUT SLASHES or else it'll be a little sppooky , NOW REMEMBER to unslash!!)
187 * Sets up $this->sword_array op with operators.
188 * This function uses $this->operator_translate_table as well as $this->default_operator
190 * @param string The input search-word string.
193 function register_and_explode_search_string($sword) {
194 $sword = trim($sword);
196 $components = $this->split($sword);
197 $s_sword = ''; // the searchword is stored here during the loop
198 if (is_array($components)) {
201 foreach ($components as $key => $val) {
202 $operator=$this->get_operator($val);
204 $lastoper = $operator;
205 } elseif (strlen($val)>1) { // A searchword MUST be at least two characters long!
206 $this->sword_array
[$i]['sword'] = $val;
207 $this->sword_array
[$i]['oper'] = ($lastoper) ?
$lastoper : $this->default_operator
;
217 * Used to split a search-word line up into elements to search for. This function will detect boolean words like AND and OR, + and -, and even find sentences encapsulated in ""
218 * This function could be re-written to be more clean and effective - yet it's not that important.
220 * @param string The raw sword string from outside
221 * @param string Special chars which are used as operators (+- is default)
222 * @param string Special chars which are deleted if the append the searchword (+-., is default)
223 * @return mixed Returns an ARRAY if there were search words, othervise the return value may be unset.
225 function split($origSword, $specchars='+-', $delchars='+.,-') {
227 $specs = '['.$this->quotemeta($specchars).']';
229 // As long as $sword is true (that means $sword MUST be reduced little by little until its empty inside the loop!)
231 if (preg_match('/^"/',$sword)) { // There was a double-quote and we will then look for the ending quote.
232 $sword = preg_replace('/^"/','',$sword); // Removes first double-quote
233 preg_match('/^[^"]*/',$sword,$reg); // Removes everything till next double-quote
234 $value[] = $reg[0]; // reg[0] is the value, should not be trimmed
235 $sword = preg_replace('/^'.$this->quotemeta($reg[0]).'/','',$sword);
236 $sword = trim(preg_replace('/^"/','',$sword)); // Removes last double-quote
237 } elseif (preg_match('/^'.$specs.'/',$sword,$reg)) {
239 $sword = trim(preg_replace('/^'.$specs.'/','',$sword)); // Removes = sign
240 } elseif (preg_match('/[\+\-]/',$sword)) { // Check if $sword contains + or -
241 // + and - shall only be interpreted as $specchars when there's whitespace before it
242 // otherwise it's included in the searchword (e.g. "know-how")
243 $a_sword = explode(' ',$sword); // explode $sword to single words
244 $word = array_shift($a_sword); // get first word
245 $word = rtrim($word, $delchars); // Delete $delchars at end of string
246 $value[] = $word; // add searchword to values
247 $sword = implode(' ',$a_sword); // re-build $sword
249 // There are no double-quotes around the value. Looking for next (space) or special char.
250 preg_match('/^[^ '.$this->quotemeta($specchars).']*/',$sword,$reg);
251 $word = rtrim(trim($reg[0]), $delchars); // Delete $delchars at end of string
253 $sword = trim(preg_replace('/^'.$this->quotemeta($reg[0]).'/','',$sword));
261 * Local version of quotemeta. This is the same as the PHP function
262 * but the vertical line, |, and minus, -, is also escaped with a slash.
264 * @param string String to pass through quotemeta()
265 * @return string Return value
267 function quotemeta($str) {
268 $str = str_replace('|','\|',quotemeta($str));
269 #$str = str_replace('-','\-',$str); // Breaks "-" which should NOT have a slash before it inside of [ ] in a regex.
274 * This creates the search-query.
275 * In TypoScript this is used for searching only records not hidden, start/endtimed and fe_grouped! (enable-fields, see tt_content)
276 * Sets $this->queryParts
278 * @param string $endClause is some extra conditions that the search must match.
279 * @return boolean Returns true no matter what - sweet isn't it!
281 * @see tslib_cObj::SEARCHRESULT()
283 function build_search_query($endClause) {
285 if (is_array($this->tables
)) {
286 $tables = $this->tables
;
289 // Primary key table is found.
290 foreach($tables as $key => $val) {
291 if ($tables[$key]['primary_key']) {$primary_table = $key;}
294 if ($primary_table) {
296 // Initialize query parts:
297 $this->queryParts
= array(
306 // Find tables / field names to select:
307 $fieldArray = array();
308 $tableArray = array();
309 foreach($tables as $key => $val) {
310 $tableArray[] = $key;
311 $resultfields = $tables[$key]['resultfields'];
312 if (is_array($resultfields)) {
313 foreach($resultfields as $key2 => $val2) {
314 $fieldArray[] = $key.'.'.$val2;
318 $this->queryParts
['SELECT'] = implode(',',$fieldArray);
319 $this->queryParts
['FROM'] = implode(',',$tableArray);
321 // Set join WHERE parts:
322 $whereArray = array();
324 $primary_table_and_key = $primary_table.'.'.$tables[$primary_table]['primary_key'];
326 foreach($tables as $key => $val) {
327 $fkey = $tables[$key]['fkey'];
329 $primKeys[] = $key.'.'.$fkey.'='.$primary_table_and_key;
332 if (count($primKeys)) {
333 $whereArray[] = '('.implode(' OR ',$primKeys).')';
336 // Additional where clause:
337 if (trim($endClause)) {
338 $whereArray[] = trim($endClause);
341 // Add search word where clause:
342 $query_part = $this->build_search_query_for_searchwords();
344 $query_part = '(0!=0)';
346 $whereArray[] = '('.$query_part.')';
348 // Implode where clauses:
349 $this->queryParts
['WHERE'] = implode(' AND ',$whereArray);
351 // Group by settings:
352 if ($this->group_by
) {
353 if ($this->group_by
== 'PRIMARY_KEY') {
354 $this->queryParts
['GROUPBY'] = $primary_table_and_key;
356 $this->queryParts
['GROUPBY'] = $this->group_by
;
364 * Creates the part of the SQL-sentence, that searches for the search-words ($this->sword_array)
366 * @return string Part of where class limiting result to the those having the search word.
369 function build_search_query_for_searchwords() {
371 if (is_array($this->sword_array
)) {
372 $main_query_part = array();
374 foreach($this->sword_array
as $key => $val) {
375 $s_sword = $this->sword_array
[$key]['sword'];
378 $sub_query_part = array();
380 $this->listOfSearchFields
='';
381 foreach($this->tables
as $key3 => $val3) {
382 $searchfields = $this->tables
[$key3]['searchfields'];
383 if (is_array($searchfields)) {
384 foreach($searchfields as $key2 => $val2) {
385 $this->listOfSearchFields
.= $key3.'.'.$val2.',';
386 $sub_query_part[] = $key3.'.'.$val2.' LIKE \'%'.$GLOBALS['TYPO3_DB']->quoteStr($s_sword, $key3).'%\'';
391 if (count($sub_query_part)) {
392 $main_query_part[] = $this->sword_array
[$key]['oper'];
393 $main_query_part[] = '('.implode(' OR ',$sub_query_part).')';
397 if (count($main_query_part)) {
398 unset($main_query_part[0]); // Remove first part anyways.
399 return implode(' ',$main_query_part);
405 * This returns an SQL search-operator (eg. AND, OR, NOT) translated from the current localized set of operators (eg. in danish OG, ELLER, IKKE).
407 * @param string The possible operator to find in the internal operator array.
408 * @return string If found, the SQL operator for the localized input operator.
411 function get_operator($operator) {
412 $operator = trim($operator);
413 $op_array = $this->operator_translate_table
;
414 if ($this->operator_translate_table_caseinsensitive
) {
415 $operator = strtolower($operator); // case-conversion is charset insensitive, but it doesn't spoil anything if input string AND operator table is already converted
417 foreach ($op_array as $key => $val) {
418 $item = $op_array[$key][0];
419 if ($this->operator_translate_table_caseinsensitive
) {
420 $item = strtolower($item); // See note above.
422 if ($operator==$item) {
423 return $op_array[$key][1];
429 * Counts the results and sets the result in $this->res_count
431 * @return boolean True, if $this->query was found
433 function count_query() {
434 if (is_array($this->queryParts
)) {
435 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($this->queryParts
['SELECT'], $this->queryParts
['FROM'], $this->queryParts
['WHERE'], $this->queryParts
['GROUPBY']);
436 $this->res_count
= $GLOBALS['TYPO3_DB']->sql_num_rows($res);
442 * Executes the search, sets result pointer in $this->result
444 * @return boolean True, if $this->query was set and query performed
446 function execute_query() {
447 if (is_array($this->queryParts
)) {
448 $this->result
= $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($this->queryParts
);
454 * Returns URL-parameters with the current search words.
455 * Used when linking to result pages so that search words can be highlighted.
457 * @return string URL-parameters with the searchwords
459 function get_searchwords() {
461 if (is_array($this->sword_array
)) {
462 foreach($this->sword_array
as $key => $val) {
463 $SWORD_PARAMS.= '&sword_list[]='.rawurlencode($val['sword']);
466 return $SWORD_PARAMS;
470 * Returns an array with the search words in
472 * @return array IF the internal sword_array contained search words it will return these, otherwise "void"
474 function get_searchwordsArray() {
475 if (is_array($this->sword_array
)) {
476 foreach($this->sword_array
as $key => $val) {
477 $swords[] = $val['sword'];
487 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE
]['XCLASS']['tslib/class.tslib_search.php']) {
488 include_once($TYPO3_CONF_VARS[TYPO3_MODE
]['XCLASS']['tslib/class.tslib_search.php']);