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