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