First CVS update in a long time - see the top of changelog. In particular the foundat...
[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 * 130: function register_tables_and_columns($requestedCols,$allowedCols)
45 * 171: function explodeCols($in)
46 * 196: function register_and_explode_search_string($sword)
47 * 229: function split($origSword, $specchars='+-')
48 * 260: function quotemeta($str)
49 * 274: function build_search_query($endClause)
50 * 360: function build_search_query_for_searchwords()
51 * 402: function get_operator($operator)
52 * 425: function count_query()
53 * 438: function execute_query()
54 * 451: function get_searchwords()
55 * 466: 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 $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 = '1';
94 var $operator_translate_table = Array ( // case-sensitiv. Defineres the words, which will be operators between words
95 Array ('+' , 'AND'),
96 Array ('-' , 'AND NOT'),
97 // english
98 Array ('AND' , 'AND'),
99 Array ('OR' , 'OR'),
100 Array ('NOT' , 'AND NOT'),
101 // danish
102 Array ('OG' , 'AND'),
103 Array ('ELLER' , 'OR'),
104 Array ('UDEN' , 'AND NOT')
105 );
106
107 // Internal
108 var $sword_array; // Contains the search-words and operators
109 var $queryParts; // Contains the query parts after processing.
110
111 var $other_where_clauses; // Addition to the whereclause. This could be used to limit search to a certain page or alike in the system.
112 var $fTable; // This is set with the foreign table that 'pages' are connected to.
113
114 var $res_offset = 0; // How many rows to offset from the beginning
115 var $res_shows = 20; // How many results to show (0 = no limit)
116 var $res_count; // Intern: How many results, there was last time (with the exact same searchstring.
117
118 var $pageIdList=''; // List of pageIds.
119
120 var $listOfSearchFields ='';
121
122 /**
123 * Creates the $this->tables-array.
124 * 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.
125 *
126 * @param string is a list (-) of columns that we want to search. This could be input from the search-form (see TypoScript documentation)
127 * @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.
128 * @return void
129 */
130 function register_tables_and_columns($requestedCols,$allowedCols) {
131 $rCols=$this->explodeCols($requestedCols);
132 $aCols=$this->explodeCols($allowedCols);
133
134 foreach ($rCols as $k => $v) {
135 $rCols[$k]=trim($v);
136 if (in_array($rCols[$k], $aCols)) {
137 $parts = explode('.',$rCols[$k]);
138 $this->tables[$parts[0]]['searchfields'][] = $parts[1];
139 }
140 }
141 $this->tables['pages']['primary_key'] = 'uid';
142 $this->tables['pages']['resultfields'][] = 'uid';
143 unset($this->tables['pages']['fkey']);
144
145 foreach ($aCols as $k => $v) {
146 $aCols[$k]=trim($v);
147 $parts = explode('.',$aCols[$k]);
148 $this->tables[$parts[0]]['resultfields'][] = $parts[1].' AS '.str_replace('.','_',$aCols[$k]);
149 $this->tables[$parts[0]]['fkey']='pid';
150 }
151
152 $this->fTable='';
153 foreach ($this->tables as $t => $v) {
154 if ($t!='pages') {
155 if (!$this->fTable) {
156 $this->fTable = $t;
157 } else {
158 unset($this->tables[$t]);
159 }
160 }
161 }
162 }
163
164 /**
165 * Function that can convert the syntax for entering which tables/fields the search should be conducted in.
166 *
167 * @param string This is the code-line defining the tables/fields to search. Syntax: '[table1].[field1]-[field2]-[field3] : [table2].[field1]-[field2]'
168 * @return array An array where the values is "[table].[field]" strings to search
169 * @see register_tables_and_columns()
170 */
171 function explodeCols($in) {
172 $theArray = explode(':',$in);
173 $out = Array();
174 while(list(,$val)=each($theArray)) {
175 $val=trim($val);
176 $parts = explode('.',$val);
177 if ($parts[0] && $parts[1]) {
178 $subparts = explode('-',$parts[1]);
179 while(list(,$piece)=each($subparts)) {
180 $piece=trim($piece);
181 if ($piece) $out[]=$parts[0].'.'.$piece;
182 }
183 }
184 }
185 return $out;
186 }
187
188 /**
189 * Takes a search-string (WITHOUT SLASHES or else it'll be a little sppooky , NOW REMEMBER to unslash!!)
190 * Sets up $this->sword_array op with operators.
191 * This function uses $this->operator_translate_table as well as $this->default_operator
192 *
193 * @param string The input search-word string.
194 * @return void
195 */
196 function register_and_explode_search_string($sword) {
197 $sword = trim($sword);
198 if ($sword) {
199 $components = $this->split($sword);
200 $s_sword = ''; // the searchword is stored here during the loop
201 if (is_array($components)) {
202 $i=0;
203 $lastoper = '';
204 reset($components);
205 while (list($key,$val) = each ($components)) {
206 $operator=$this->get_operator($val);
207 if ($operator) {
208 $lastoper = $operator;
209 } elseif (strlen($val)>1) { // A searchword MUST be at least two characters long!
210 $this->sword_array[$i]['sword'] = $val;
211 $this->sword_array[$i]['oper'] = ($lastoper) ? $lastoper : $this->default_operator;
212 $lastoper = '';
213 $i++;
214 }
215 }
216 }
217 }
218 }
219
220 /**
221 * 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 ""
222 * This function also has the charm of still containing some of the original comments - in danish!
223 * This function could be re-written to be more clean and effective - yet it's not that important.
224 *
225 * @param string The raw sword string from outside
226 * @param string Special chars which are used as operators (+- is default)
227 * @return mixed Returns an ARRAY if there were search words, othervise the return value may be unset.
228 */
229 function split($origSword, $specchars='+-') {
230 $sword = $origSword;
231 $specs = '['.$this->quotemeta($specchars).']';
232
233 // As long as $sword is true (that means $sword MUST be reduced little by little until its empty inside the loop!)
234 while ($sword) {
235 if (ereg('^"',$sword)) { // There was a double-quote and we will then look for the ending quote.
236 $sword = ereg_replace('^"','',$sword); // Removes first double-quote
237 ereg('^[^"]*',$sword,$reg); // Removes everything till next double-quote
238 $value[] = $reg[0]; // reg[0] is the value, should not be trimmed
239 $sword = ereg_replace('^'.$this->quotemeta($reg[0]),'',$sword);
240 $sword = trim(ereg_replace('^"','',$sword)); // Removes last double-quote
241 } elseif (ereg('^'.$specs,$sword,$reg)) {
242 $value[] = $reg[0];
243 $sword = trim(ereg_replace('^'.$specs,'',$sword)); // Removes = sign
244 } else {
245 // There are no double-quotes around the value. Looking for next (space) ' ' or '>'
246 ereg('^[^ '.$this->quotemeta($specchars).']*',$sword,$reg);
247 $value[] = trim($reg[0]);
248 $sword = trim(ereg_replace('^'.$this->quotemeta($reg[0]),'',$sword));
249 }
250 }
251 return $value;
252 }
253
254 /**
255 * Local version of quotemeta. This is the same as the PHP function but the vertical line, |, is also escaped with a slash.
256 *
257 * @param string String to pass through quotemeta()
258 * @return string Return value
259 */
260 function quotemeta($str) {
261 return str_replace('|','\|',quotemeta($str));
262 }
263
264 /**
265 * This creates the search-query.
266 * In TypoScript this is used for searching only records not hidden, start/endtimed and fe_grouped! (enable-fields, see tt_content)
267 * Sets $this->queryParts
268 *
269 * @param string $endClause is some extra conditions that the search must match.
270 * @return boolean Returns true no matter what - sweet isn't it!
271 * @access private
272 * @see tslib_cObj::SEARCHRESULT()
273 */
274 function build_search_query($endClause) {
275
276 if (is_array($this->tables)) {
277 $tables = $this->tables;
278 $primary_table = '';
279
280 // Primary key table is found.
281 foreach($tables as $key => $val) {
282 if ($tables[$key]['primary_key']) {$primary_table = $key;}
283 }
284
285 if ($primary_table) {
286
287 // Initialize query parts:
288 $this->queryParts = array(
289 'SELECT' => '',
290 'FROM' => '',
291 'WHERE' => '',
292 'GROUPBY' => '',
293 'ORDERBY' => '',
294 'LIMIT' => '',
295 );
296
297 // Find tables / field names to select:
298 $fieldArray = array();
299 $tableArray = array();
300 foreach($tables as $key => $val) {
301 $tableArray[] = $key;
302 $resultfields = $tables[$key]['resultfields'];
303 if (is_array($resultfields)) {
304 foreach($resultfields as $key2 => $val2) {
305 $fieldArray[] = $key.'.'.$val2;
306 }
307 }
308 }
309 $this->queryParts['SELECT'] = implode(',',$fieldArray);
310 $this->queryParts['FROM'] = implode(',',$tableArray);
311
312 // Set join WHERE parts:
313 $whereArray = array();
314
315 $primary_table_and_key = $primary_table.'.'.$tables[$primary_table]['primary_key'];
316 $primKeys=Array();
317 foreach($tables as $key => $val) {
318 $fkey = $tables[$key]['fkey'];
319 if ($fkey) {
320 $primKeys[]=$key.'.'.$fkey.'='.$primary_table_and_key;
321 }
322 }
323 if (count($primKeys)) {
324 $whereArray[] = '('.implode(' OR ',$primKeys).')';
325 }
326
327 // Additional where clause:
328 if (trim($endClause)) {
329 $whereArray[] = trim($endClause);
330 }
331
332 // Add search word where clause:
333 $query_part = $this->build_search_query_for_searchwords();
334 if (!$query_part) {
335 $query_part='(0!=0)';
336 }
337 $whereArray[] = '('.$query_part.')';
338
339 // Implode where clauses:
340 $this->queryParts['WHERE'] = implode(' AND ',$whereArray);
341
342 // Group by settings:
343 if ($this->group_by) {
344 if ($this->group_by == 'PRIMARY_KEY') {
345 $this->queryParts['GROUPBY'] = $primary_table_and_key;
346 } else {
347 $this->queryParts['GROUPBY'] = $this->group_by;
348 }
349 }
350 }
351 }
352 }
353
354 /**
355 * Creates the part of the SQL-sentence, that searches for the search-words ($this->sword_array)
356 *
357 * @return string Part of where class limiting result to the those having the search word.
358 * @access private
359 */
360 function build_search_query_for_searchwords() {
361
362 if (is_array($this->sword_array)) {
363 $main_query_part = array();
364
365 foreach($this->sword_array as $key => $val) {
366 $s_sword = $this->sword_array[$key]['sword'];
367
368 // Get subQueryPart
369 $sub_query_part = array();
370
371 $this->listOfSearchFields='';
372 foreach($this->tables as $key3 => $val3) {
373 $searchfields = $this->tables[$key3]['searchfields'];
374 if (is_array($searchfields)) {
375 foreach($searchfields as $key2 => $val2) {
376 $this->listOfSearchFields.= $key3.'.'.$val2.',';
377 $sub_query_part[] = $key3.'.'.$val2.' LIKE "%'.$GLOBALS['TYPO3_DB']->quoteStr($s_sword, $key3).'%"';
378 }
379 }
380 }
381
382 if (count($sub_query_part)) {
383 $main_query_part[] = $this->sword_array[$key]['oper'];
384 $main_query_part[] = '('.implode(' OR ',$sub_query_part).')';
385 }
386 }
387
388 if (count($main_query_part)) {
389 unset($main_query_part[0]); // Remove first part anyways.
390 return implode(' ',$main_query_part);
391 }
392 }
393 }
394
395 /**
396 * This returns an SQL search-operator (eg. AND, OR, NOT) translated from the current localized set of operators (eg. in danish OG, ELLER, IKKE).
397 *
398 * @param string The possible operator to find in the internal operator array.
399 * @return string If found, the SQL operator for the localized input operator.
400 * @access private
401 */
402 function get_operator($operator) {
403 $operator = trim($operator);
404 $op_array = $this->operator_translate_table;
405 reset ($op_array);
406 if ($this->operator_translate_table_caseinsensitive) {
407 $operator = strtoupper($operator);
408 }
409 while (list($key,$val) = each($op_array)) {
410 $item = $op_array[$key][0];
411 if ($this->operator_translate_table_caseinsensitive) {
412 $item = strtoupper($item);
413 }
414 if ($operator==$item) {
415 return $op_array[$key][1];
416 }
417 }
418 }
419
420 /**
421 * Counts the results and sets the result in $this->res_count
422 *
423 * @return boolean True, if $this->query was found
424 */
425 function count_query() {
426 if (is_array($this->queryParts)) {
427 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($this->queryParts['SELECT'], $this->queryParts['FROM'], $this->queryParts['WHERE'], $this->queryParts['GROUPBY']);
428 $this->res_count = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
429 return TRUE;
430 }
431 }
432
433 /**
434 * Executes the search, sets result pointer in $this->result
435 *
436 * @return boolean True, if $this->query was set and query performed
437 */
438 function execute_query() {
439 if (is_array($this->queryParts)) {
440 $this->result = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($this->queryParts);
441 return TRUE;
442 }
443 }
444
445 /**
446 * Returns URL-parameters with the current search words.
447 * Used when linking to result pages so that search words can be highlighted.
448 *
449 * @return string URL-parameters with the searchwords
450 */
451 function get_searchwords() {
452 $SWORD_PARAMS='';
453 if (is_array($this->sword_array)) {
454 foreach($this->sword_array as $key => $val) {
455 $SWORD_PARAMS.='&sword_list[]='.rawurlencode($val['sword']);
456 }
457 }
458 return $SWORD_PARAMS;
459 }
460
461 /**
462 * Returns an array with the search words in
463 *
464 * @return array IF the internal sword_array contained search words it will return these, otherwise "void"
465 */
466 function get_searchwordsArray() {
467 if (is_array($this->sword_array)) {
468 foreach($this->sword_array as $key => $val) {
469 $swords[]=$val['sword'];
470 }
471 }
472 return $swords;
473 }
474 }
475
476
477
478
479 if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['tslib/class.tslib_search.php']) {
480 include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['tslib/class.tslib_search.php']);
481 }
482
483 ?>