Updated copyright notices to show "2004"
[Packages/TYPO3.CMS.git] / typo3 / sysext / cms / tslib / class.tslib_search.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 1999-2004 Kasper Skaarhoj (kasper@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 * 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"
31 *
32 * $Id$
33 * Revised for TYPO3 3.6 June/2003 by Kasper Skaarhoj
34 *
35 * @author Kasper Skaarhoj <kasper@typo3.com>
36 * @author Rene Fritz <r.fritz@colorcube.de>
37 */
38 /**
39 * [CLASS/FUNCTION INDEX of SCRIPT]
40 *
41 *
42 *
43 * 88: class tslib_search
44 * 138: function register_tables_and_columns($requestedCols,$allowedCols)
45 * 180: function explodeCols($in)
46 * 205: function register_and_explode_search_string ($sword)
47 * 238: function split($origSword, $specchars='+-')
48 * 269: function quotemeta($str)
49 * 283: function build_search_query ($endClause)
50 * 366: function build_search_query_for_searchwords ()
51 * 415: function get_operator ($operator)
52 * 438: function count_query ()
53 * 452: function execute_query()
54 * 468: function get_searchwords()
55 * 484: function get_searchwordsArray()
56 *
57 * TOTAL FUNCTIONS: 12
58 * (This index is automatically created/updated by the extension "extdeveval")
59 *
60 */
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80 /**
81 * Search class used for the content object SEARCHRESULT
82 *
83 * @author Kasper Skaarhoj <kasper@typo3.com>
84 * @package TYPO3
85 * @subpackage tslib
86 * @see tslib_cObj::SEARCHRESULT()
87 */
88 class tslib_search {
89 var $tables = Array ();
90
91 var $standalone = ''; // true / false - if the searchresult MAY NOT be a part-string (doesn't work yet. Does not find words with parentheses or periods before/after....beginning/end of a line is also a problem!)
92 var $mixedcase = 'yes'; // true / false - matches both upper and lower case (doesn't work if you disable. Matches all cases currently)
93
94 var $order_by = ''; // ORDER BY part of the query. (field-reference, eg. 'pages.uid'
95 var $group_by = 'PRIMARY_KEY'; // Alternatively 'PRIMARY_KEY'; sorting by primary key
96
97 var $default_operator = 'AND'; // Standard SQL-operator between words
98 var $operator_translate_table_caseinsensitive = '1';
99 var $operator_translate_table = Array ( // case-sensitiv. Defineres the words, which will be operators between words
100 Array ('+' , 'AND'),
101 Array ('-' , 'AND NOT'),
102 // english
103 Array ('AND' , 'AND'),
104 Array ('OR' , 'OR'),
105 Array ('NOT' , 'AND NOT'),
106 // danish
107 Array ('OG' , 'AND'),
108 Array ('ELLER' , 'OR'),
109 Array ('UDEN' , 'AND NOT')
110 );
111
112 // Internal
113 var $sword_array; // Contains the search-words and operators
114 var $query_begin = ''; // Beginning of query
115 var $query_end = ''; // Ending of query
116
117 var $query; // Contains the final query after processing.
118
119 var $other_where_clauses; // Addition to the whereclause. This could be used to limit search to a certain page or alike in the system.
120 var $fTable; // This is set with the foreign table that 'pages' are connected to.
121
122 var $res_offset = 0; // How many rows to offset from the beginning
123 var $res_shows = 20; // How many results to show (0 = no limit)
124 var $res_count; // Intern: How many results, there was last time (with the exact same searchstring.
125
126 var $pageIdList=''; // List of pageIds.
127
128 var $listOfSearchFields ='';
129
130 /**
131 * Creates the $this->tables-array.
132 * 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.
133 *
134 * @param string is a list (-) of columns that we want to search. This could be input from the search-form (see TypoScript documentation)
135 * @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.
136 * @return void
137 */
138 function register_tables_and_columns($requestedCols,$allowedCols) {
139 $rCols=$this->explodeCols($requestedCols);
140 $aCols=$this->explodeCols($allowedCols);
141
142 foreach ($rCols as $k => $v) {
143 $rCols[$k]=trim($v);
144 if (in_array($rCols[$k], $aCols)) {
145 $parts = explode('.',$rCols[$k]);
146 $this->tables[$parts[0]]['searchfields'][] = $parts[1];
147 // $this->tables[$parts[0]]['resultfields']['uid']='uid'; // Cannot set this, because then the link to the searched page will not be correct! Must set otherwise
148 }
149 }
150 $this->tables['pages']['primary_key'] = 'uid';
151 $this->tables['pages']['resultfields'][]='uid';
152 unset($this->tables['pages']['fkey']);
153
154 foreach ($aCols as $k => $v) {
155 $aCols[$k]=trim($v);
156 $parts = explode('.',$aCols[$k]);
157 $this->tables[$parts[0]]['resultfields'][] = $parts[1].' AS '.str_replace('.','_',$aCols[$k]);
158 $this->tables[$parts[0]]['fkey']='pid';
159 }
160
161 $this->fTable='';
162 foreach ($this->tables as $t => $v) {
163 if ($t!='pages') {
164 if (!$this->fTable) {
165 $this->fTable=$t;
166 } else {
167 unset($this->tables[$t]);
168 }
169 }
170 }
171 }
172
173 /**
174 * Function that can convert the syntax for entering which tables/fields the search should be conducted in.
175 *
176 * @param string This is the code-line defining the tables/fields to search. Syntax: '[table1].[field1]-[field2]-[field3] : [table2].[field1]-[field2]'
177 * @return array An array where the values is "[table].[field]" strings to search
178 * @see register_tables_and_columns()
179 */
180 function explodeCols($in) {
181 $theArray = explode(':',$in);
182 $out = Array();
183 while(list(,$val)=each($theArray)) {
184 $val=trim($val);
185 $parts = explode('.',$val);
186 if ($parts[0] && $parts[1]) {
187 $subparts = explode('-',$parts[1]);
188 while(list(,$piece)=each($subparts)) {
189 $piece=trim($piece);
190 if ($piece) $out[]=$parts[0].'.'.$piece;
191 }
192 }
193 }
194 return $out;
195 }
196
197 /**
198 * Takes a search-string (WITHOUT SLASHES or else it'll be a little sppooky , NOW REMEMBER to unslash!!)
199 * Sets up $this->sword_array op with operators.
200 * This function uses $this->operator_translate_table as well as $this->default_operator
201 *
202 * @param string The input search-word string.
203 * @return void
204 */
205 function register_and_explode_search_string ($sword) {
206 $sword = trim($sword);
207 if ($sword) {
208 $components = $this->split($sword);
209 $s_sword = ''; // the searchword is stored here during the loop
210 if (is_array($components)) {
211 $i=0;
212 $lastoper = '';
213 reset($components);
214 while (list($key,$val) = each ($components)) {
215 $operator=$this->get_operator($val);
216 if ($operator) {
217 $lastoper = $operator;
218 } elseif (strlen($val)>1) { // A searchword MUST be at least two characters long!
219 $this->sword_array[$i]['sword'] = $val;
220 $this->sword_array[$i]['oper'] = ($lastoper) ? $lastoper : $this->default_operator;
221 $lastoper = '';
222 $i++;
223 }
224 }
225 }
226 }
227 }
228
229 /**
230 * 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 ""
231 * This function also has the charm of still containing some of the original comments - in danish!
232 * This function could be re-written to be more clean and effective - yet it's not that important.
233 *
234 * @param string The raw sword string from outside
235 * @param string Special chars which are used as operators (+- is default)
236 * @return mixed Returns an ARRAY if there were search words, othervise the return value may be unset.
237 */
238 function split($origSword, $specchars='+-') {
239 $sword = $origSword;
240 $specs = '['.$this->quotemeta($specchars).']';
241
242 // As long as $sword is true (that means $sword MUST be reduced little by little until its empty inside the loop!)
243 while ($sword) {
244 if (ereg('^"',$sword)) { // There was a double-quote and we will then look for the ending quote.
245 $sword = ereg_replace('^"','',$sword); // Fjerner først gåseøje.
246 ereg('^[^"]*',$sword,$reg); // Tager alt indtil næste gåseøje
247 $value[] = $reg[0]; // reg[0] er lig med værdien!! Skal ikke trimmes
248 $sword = ereg_replace('^'.$this->quotemeta($reg[0]),'',$sword);
249 $sword = trim(ereg_replace('^"','',$sword)); // Fjerner det sidste gåseøje.
250 } elseif (ereg('^'.$specs,$sword,$reg)) {
251 $value[] = $reg[0];
252 $sword = trim(ereg_replace('^'.$specs,'',$sword)); // Fjerner = tegn.
253 } else {
254 // Der er ikke gåseøjne om værdien. Der ledes efter næste ' ' eller '>'
255 ereg('^[^ '.$this->quotemeta($specchars).']*',$sword,$reg);
256 $value[] = trim($reg[0]);
257 $sword = trim(ereg_replace('^'.$this->quotemeta($reg[0]),'',$sword));
258 }
259 }
260 return $value;
261 }
262
263 /**
264 * Local version of quotemeta. This is the same as the PHP function but the vertical line, |, is also escaped with a slash.
265 *
266 * @param string String to pass through quotemeta()
267 * @return string Return value
268 */
269 function quotemeta($str) {
270 return str_replace('|','\|',quotemeta($str));
271 }
272
273 /**
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->query
277 *
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!
280 * @access private
281 * @see tslib_cObj::SEARCHRESULT()
282 */
283 function build_search_query ($endClause) {
284 if (is_array($this->tables)) {
285 $tables = $this->tables;
286 $primary_table = '';
287 $query = 'SELECT';
288 // Primary key table is found.
289 reset($tables);
290 while (list($key,$val) = each($tables)) {
291 if ($tables[$key]['primary_key']) {$primary_table = $key;}
292 }
293 if ($primary_table) {
294 reset($tables);
295 while (list($key,$val) = each($tables)) {
296 $resultfields = $tables[$key]['resultfields'];
297 if (is_array($resultfields)) {
298 reset($resultfields);
299 while (list($key2,$val2) = each($resultfields)) {
300 $query.= ' '.$key.'.'.$val2.',';
301 }
302 }
303 }
304
305 $query = t3lib_div::rm_endcomma($query);
306 $query.= ' FROM';
307
308 reset($tables);
309 while (list($key,$val) = each($tables)) {
310 $query.= ' '.$key.',';
311 }
312
313 $query = t3lib_div::rm_endcomma($query);
314 $query.= ' WHERE';
315
316 reset($tables);
317 $primary_table_and_key = $primary_table.'.'.$tables[$primary_table]['primary_key'];
318 $primKeys=Array();
319 while (list($key,$val) = each($tables)) {
320 $fkey = $tables[$key]['fkey'];
321 if ($fkey) {
322 $primKeys[]=$key.'.'.$fkey.'='.$primary_table_and_key;
323 }
324 }
325 if (count($primKeys)) {
326 $query.='('.implode($primKeys,' OR ').')';
327 }
328 if (!ereg('WHERE$',trim($query))) {
329 $query.=' AND';
330 }
331
332 $tempClause = trim($endClause);
333 if ($tempClause) {
334 $query.= ' ('.$tempClause.') AND';
335 }
336
337 $query.= ' (';
338 $this->query_begin = $query;
339
340 if ($this->group_by) {
341 if ($this->group_by == 'PRIMARY_KEY') {
342 $this->query_end = ') GROUP BY '.$primary_table_and_key;
343 } else {
344 $this->query_end = ') GROUP BY '.$this->group_by;
345 }
346 } else {
347 $this->query_end = ')';
348 }
349 }
350 }
351
352 $query_part = $this->build_search_query_for_searchwords();
353 if (!$query_part) {
354 $query_part='(0!=0)';
355 }
356 $this->query = $this->query_begin.$query_part.$this->query_end;
357 return true;
358 }
359
360 /**
361 * Creates the part of the SQL-sentence, that searches for the search-words ($this->sword_array)
362 *
363 * @return string Part of where class limiting result to the those having the search word.
364 * @access private
365 */
366 function build_search_query_for_searchwords () {
367 $tables = $this->tables;
368 $sword_array = $this->sword_array;
369 $query_part = '';
370 $sp='';
371 if ($this->standalone) {$sp=' ';} // Der indsættes et space foran og efter ordet, hvis det SKAL stå alene. Dette er dog ikke korrekt implementeret, fordi det ikke finder ord i starten, slutningen og i parenteser osv. Egentlig skal der være check på noget om der er alfanumeriske værdier før eller efter!!
372 if (is_array($sword_array)) {
373 reset($sword_array);
374 while (list($key,$val) = each($sword_array)) {
375 $s_sword = $sword_array[$key]['sword'];
376 // Get subQueryPart
377 $sub_query_part='';
378 reset ($tables);
379 $this->listOfSearchFields='';
380 while (list($key3,$val3) = each($tables)) {
381 $searchfields = $tables[$key3]['searchfields'];
382 if (is_array($searchfields)) {
383 reset ($searchfields);
384 while (list($key2,$val2) = each($searchfields)) {
385 $this->listOfSearchFields.=$key3.'.'.$val2.',';
386 $sub_query_part.= ' '.$key3.'.'.$val2.' LIKE "%'.$sp.addslashes($s_sword).$sp.'%" OR';
387 }
388 }
389 }
390 $sub_query_part = trim(ereg_replace('OR$','',$sub_query_part));
391
392 if ($sub_query_part) {
393 if ($query_part != '') {
394 $query_part.= ' '.$sword_array[$key]['oper'].' ('.$sub_query_part.')';
395 } else {
396 $query_part.= '('.$sub_query_part.')';
397 }
398 }
399 }
400 $query_part = trim($query_part);
401 if (!$query_part || trim($query_part)=='()') {
402 $query_part = '';
403 }
404 return $query_part;
405 }
406 }
407
408 /**
409 * This returns an SQL search-operator (eg. AND, OR, NOT) translated from the current localized set of operators (eg. in danish OG, ELLER, IKKE).
410 *
411 * @param string The possible operator to find in the internal operator array.
412 * @return string If found, the SQL operator for the localized input operator.
413 * @access private
414 */
415 function get_operator ($operator) {
416 $operator = trim($operator);
417 $op_array = $this->operator_translate_table;
418 reset ($op_array);
419 if ($this->operator_translate_table_caseinsensitive) {
420 $operator = strtoupper($operator);
421 }
422 while (list($key,$val) = each($op_array)) {
423 $item = $op_array[$key][0];
424 if ($this->operator_translate_table_caseinsensitive) {
425 $item = strtoupper($item);
426 }
427 if ($operator==$item) {
428 return $op_array[$key][1];
429 }
430 }
431 }
432
433 /**
434 * Counts the results and sets the result in $this->res_count
435 *
436 * @return boolean True, if $this->query was found
437 */
438 function count_query () {
439 if ($this->query) {
440 $res = mysql(TYPO3_db, $this->query);
441 echo mysql_error();
442 $this->res_count = mysql_num_rows($res);
443 return true;
444 }
445 }
446
447 /**
448 * Executes the search, sets result pointer in $this->result
449 *
450 * @return boolean True, if $this->query was set and query performed
451 */
452 function execute_query() {
453 $query = $this->query;
454 if ($query) {
455 if ($this->order_by) $query.= ' ORDER BY '.$this->order_by;
456 $this->result = mysql(TYPO3_db, $query);
457 echo mysql_error();
458 return true;
459 }
460 }
461
462 /**
463 * Returns URL-parameters with the current search words.
464 * Used when linking to result pages so that search words can be highlighted.
465 *
466 * @return string URL-parameters with the searchwords
467 */
468 function get_searchwords() {
469 $SWORD_PARAMS='';
470 if (is_array($this->sword_array)) {
471 reset($this->sword_array);
472 while (list($key,$val)=each($this->sword_array)) {
473 $SWORD_PARAMS.='&sword_list[]='.rawurlencode($val['sword']);
474 }
475 }
476 return $SWORD_PARAMS;
477 }
478
479 /**
480 * Returns an array with the search words in
481 *
482 * @return array IF the internal sword_array contained search words it will return these, otherwise "void"
483 */
484 function get_searchwordsArray() {
485 if (is_array($this->sword_array)) {
486 reset($this->sword_array);
487 while (list($key,$val)=each($this->sword_array)) {
488 $swords[]=$val['sword'];
489 }
490 }
491 return $swords;
492 }
493 }
494
495
496
497
498 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['tslib/class.tslib_search.php']) {
499 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['tslib/class.tslib_search.php']);
500 }
501
502 ?>