TYPO3 API  SVNRelease
class.tslib_search.php
Go to the documentation of this file.
00001 <?php
00002 /***************************************************************
00003 *  Copyright notice
00004 *
00005 *  (c) 1999-2011 Kasper Skårhøj (kasperYYYY@typo3.com)
00006 *  All rights reserved
00007 *
00008 *  This script is part of the TYPO3 project. The TYPO3 project is
00009 *  free software; you can redistribute it and/or modify
00010 *  it under the terms of the GNU General Public License as published by
00011 *  the Free Software Foundation; either version 2 of the License, or
00012 *  (at your option) any later version.
00013 *
00014 *  The GNU General Public License can be found at
00015 *  http://www.gnu.org/copyleft/gpl.html.
00016 *  A copy is found in the textfile GPL.txt and important notices to the license
00017 *  from the author is found in LICENSE.txt distributed with these scripts.
00018 *
00019 *
00020 *  This script is distributed in the hope that it will be useful,
00021 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00022 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00023 *  GNU General Public License for more details.
00024 *
00025 *  This copyright notice MUST APPEAR in all copies of the script!
00026 ***************************************************************/
00027 /**
00028  * Searching in database tables, typ. "pages" and "tt_content"
00029  * Used to generate search queries for TypoScript.
00030  * The class is included from "class.tslib_pagegen.php" based on whether there has been detected content in the GPvar "sword"
00031  *
00032  * $Id: class.tslib_search.php 10120 2011-01-18 20:03:36Z ohader $
00033  * Revised for TYPO3 3.6 June/2003 by Kasper Skårhøj
00034  *
00035  * @author  Kasper Skårhøj <kasperYYYY@typo3.com>
00036  * @author  René Fritz <r.fritz@colorcube.de>
00037  */
00038 /**
00039  * [CLASS/FUNCTION INDEX of SCRIPT]
00040  *
00041  *
00042  *
00043  *   88: class tslib_search
00044  *  127:     function register_tables_and_columns($requestedCols,$allowedCols)
00045  *  168:     function explodeCols($in)
00046  *  193:     function register_and_explode_search_string($sword)
00047  *  226:     function split($origSword, $specchars='+-', $delchars='+.,-')
00048  *  269:     function quotemeta($str)
00049  *  285:     function build_search_query($endClause)
00050  *  371:     function build_search_query_for_searchwords()
00051  *  413:     function get_operator($operator)
00052  *  436:     function count_query()
00053  *  449:     function execute_query()
00054  *  462:     function get_searchwords()
00055  *  477:     function get_searchwordsArray()
00056  *
00057  * TOTAL FUNCTIONS: 12
00058  * (This index is automatically created/updated by the extension "extdeveval")
00059  *
00060  */
00061 
00062 
00063 
00064 
00065 
00066 
00067 
00068 
00069 
00070 
00071 
00072 
00073 
00074 
00075 
00076 
00077 
00078 
00079 
00080 /**
00081  * Search class used for the content object SEARCHRESULT
00082  *
00083  * @author  Kasper Skårhøj <kasperYYYY@typo3.com>
00084  * @package TYPO3
00085  * @subpackage tslib
00086  * @see tslib_cObj::SEARCHRESULT()
00087  */
00088 class tslib_search {
00089     var $tables = Array ();
00090 
00091     var $group_by = 'PRIMARY_KEY';                          // Alternatively 'PRIMARY_KEY'; sorting by primary key
00092     var $default_operator = 'AND';                          // Standard SQL-operator between words
00093     var $operator_translate_table_caseinsensitive = TRUE;
00094     var $operator_translate_table = Array (                 // case-sensitiv. Defineres the words, which will be operators between words
00095         Array ('+' , 'AND'),
00096         Array ('|' , 'AND'),
00097         Array ('-' , 'AND NOT'),
00098             // english
00099         Array ('and' , 'AND'),
00100         Array ('or' , 'OR'),
00101         Array ('not' , 'AND NOT'),
00102     );
00103 
00104     // Internal
00105     var $sword_array;       // Contains the search-words and operators
00106     var $queryParts;        // Contains the query parts after processing.
00107 
00108     var $other_where_clauses;   // Addition to the whereclause. This could be used to limit search to a certain page or alike in the system.
00109     var $fTable;        // This is set with the foreign table that 'pages' are connected to.
00110 
00111     var $res_offset = 0;    // How many rows to offset from the beginning
00112     var $res_shows = 20;    // How many results to show (0 = no limit)
00113     var $res_count;         // Intern: How many results, there was last time (with the exact same searchstring.
00114 
00115     var $pageIdList='';     // List of pageIds.
00116 
00117     var $listOfSearchFields ='';
00118 
00119     /**
00120      * Creates the $this->tables-array.
00121      * 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.
00122      *
00123      * @param   string      is a list (-) of columns that we want to search. This could be input from the search-form (see TypoScript documentation)
00124      * @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.
00125      * @return  void
00126      */
00127     function register_tables_and_columns($requestedCols,$allowedCols)   {
00128         $rCols=$this->explodeCols($requestedCols);
00129         $aCols=$this->explodeCols($allowedCols);
00130 
00131         foreach ($rCols as $k => $v)    {
00132             $rCols[$k]=trim($v);
00133             if (in_array($rCols[$k], $aCols))   {
00134                 $parts = explode('.',$rCols[$k]);
00135                 $this->tables[$parts[0]]['searchfields'][] = $parts[1];
00136             }
00137         }
00138         $this->tables['pages']['primary_key'] = 'uid';
00139         $this->tables['pages']['resultfields'][] = 'uid';
00140         unset($this->tables['pages']['fkey']);
00141 
00142         foreach ($aCols as $k => $v)    {
00143             $aCols[$k]=trim($v);
00144             $parts = explode('.',$aCols[$k]);
00145             $this->tables[$parts[0]]['resultfields'][] = $parts[1].' AS '.str_replace('.','_',$aCols[$k]);
00146             $this->tables[$parts[0]]['fkey']='pid';
00147         }
00148 
00149         $this->fTable='';
00150         foreach ($this->tables as $t => $v) {
00151             if ($t!='pages')    {
00152                 if (!$this->fTable) {
00153                     $this->fTable = $t;
00154                 } else {
00155                     unset($this->tables[$t]);
00156                 }
00157             }
00158         }
00159     }
00160 
00161     /**
00162      * Function that can convert the syntax for entering which tables/fields the search should be conducted in.
00163      *
00164      * @param   string      This is the code-line defining the tables/fields to search. Syntax: '[table1].[field1]-[field2]-[field3] : [table2].[field1]-[field2]'
00165      * @return  array       An array where the values is "[table].[field]" strings to search
00166      * @see register_tables_and_columns()
00167      */
00168     function explodeCols($in)   {
00169         $theArray = explode(':',$in);
00170         $out = Array();
00171         foreach ($theArray as $val) {
00172             $val=trim($val);
00173             $parts = explode('.',$val);
00174             if ($parts[0] && $parts[1]) {
00175                 $subparts = explode('-',$parts[1]);
00176                 foreach ($subparts as $piece) {
00177                     $piece=trim($piece);
00178                     if ($piece)     $out[]=$parts[0].'.'.$piece;
00179                 }
00180             }
00181         }
00182         return $out;
00183     }
00184 
00185     /**
00186      * Takes a search-string (WITHOUT SLASHES or else it'll be a little sppooky , NOW REMEMBER to unslash!!)
00187      * Sets up $this->sword_array op with operators.
00188      * This function uses $this->operator_translate_table as well as $this->default_operator
00189      *
00190      * @param   string      The input search-word string.
00191      * @return  void
00192      */
00193     function register_and_explode_search_string($sword) {
00194         $sword = trim($sword);
00195         if ($sword) {
00196             $components = $this->split($sword);
00197             $s_sword = '';   // the searchword is stored here during the loop
00198             if (is_array($components))  {
00199                 $i=0;
00200                 $lastoper = '';
00201                 foreach ($components as $key => $val) {
00202                     $operator=$this->get_operator($val);
00203                     if ($operator)  {
00204                         $lastoper = $operator;
00205                     } elseif (strlen($val)>1) {     // A searchword MUST be at least two characters long!
00206                         $this->sword_array[$i]['sword'] = $val;
00207                         $this->sword_array[$i]['oper'] = ($lastoper) ? $lastoper : $this->default_operator;
00208                         $lastoper = '';
00209                         $i++;
00210                     }
00211                 }
00212             }
00213         }
00214     }
00215 
00216     /**
00217      * 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 ""
00218      * This function could be re-written to be more clean and effective - yet it's not that important.
00219      *
00220      * @param   string      The raw sword string from outside
00221      * @param   string      Special chars which are used as operators (+- is default)
00222      * @param   string      Special chars which are deleted if the append the searchword (+-., is default)
00223      * @return  mixed       Returns an ARRAY if there were search words, othervise the return value may be unset.
00224      */
00225     function split($origSword, $specchars='+-', $delchars='+.,-')   {
00226         $sword = $origSword;
00227         $specs = '['.$this->quotemeta($specchars).']';
00228 
00229             // As long as $sword is true (that means $sword MUST be reduced little by little until its empty inside the loop!)
00230         while ($sword)  {
00231             if (preg_match('/^"/',$sword))  {       // There was a double-quote and we will then look for the ending quote.
00232                 $sword = preg_replace('/^"/','',$sword);        // Removes first double-quote
00233                 preg_match('/^[^"]*/',$sword,$reg);  // Removes everything till next double-quote
00234                 $value[] = $reg[0];  // reg[0] is the value, should not be trimmed
00235                 $sword = preg_replace('/^'.$this->quotemeta($reg[0]).'/','',$sword);
00236                 $sword = trim(preg_replace('/^"/','',$sword));      // Removes last double-quote
00237             } elseif (preg_match('/^'.$specs.'/',$sword,$reg)) {
00238                 $value[] = $reg[0];
00239                 $sword = trim(preg_replace('/^'.$specs.'/','',$sword));     // Removes = sign
00240             } elseif (preg_match('/[\+\-]/',$sword)) {  // Check if $sword contains + or -
00241                     // + and - shall only be interpreted as $specchars when there's whitespace before it
00242                     // otherwise it's included in the searchword (e.g. "know-how")
00243                 $a_sword = explode(' ',$sword); // explode $sword to single words
00244                 $word = array_shift($a_sword);  // get first word
00245                 $word = rtrim($word, $delchars);        // Delete $delchars at end of string
00246                 $value[] = $word;   // add searchword to values
00247                 $sword = implode(' ',$a_sword); // re-build $sword
00248             } else {
00249                     // There are no double-quotes around the value. Looking for next (space) or special char.
00250                 preg_match('/^[^ '.$this->quotemeta($specchars).']*/',$sword,$reg);
00251                 $word = rtrim(trim($reg[0]), $delchars);        // Delete $delchars at end of string
00252                 $value[] = $word;
00253                 $sword = trim(preg_replace('/^'.$this->quotemeta($reg[0]).'/','',$sword));
00254             }
00255         }
00256 
00257         return $value;
00258     }
00259 
00260     /**
00261      * Local version of quotemeta. This is the same as the PHP function
00262      * but the vertical line, |, and minus, -, is also escaped with a slash.
00263      *
00264      * @param   string      String to pass through quotemeta()
00265      * @return  string      Return value
00266      */
00267     function quotemeta($str)    {
00268         $str = str_replace('|','\|',quotemeta($str));
00269         #$str = str_replace('-','\-',$str);     // Breaks "-" which should NOT have a slash before it inside of [ ] in a regex.
00270         return $str;
00271     }
00272 
00273     /**
00274      * This creates the search-query.
00275      * In TypoScript this is used for searching only records not hidden, start/endtimed and fe_grouped! (enable-fields, see tt_content)
00276      * Sets $this->queryParts
00277      *
00278      * @param   string      $endClause is some extra conditions that the search must match.
00279      * @return  boolean     Returns true no matter what - sweet isn't it!
00280      * @access private
00281      * @see tslib_cObj::SEARCHRESULT()
00282      */
00283     function build_search_query($endClause) {
00284 
00285         if (is_array($this->tables))    {
00286             $tables = $this->tables;
00287             $primary_table = '';
00288 
00289                 // Primary key table is found.
00290             foreach($tables as $key => $val)    {
00291                 if ($tables[$key]['primary_key'])   {$primary_table = $key;}
00292             }
00293 
00294             if ($primary_table) {
00295 
00296                     // Initialize query parts:
00297                 $this->queryParts = array(
00298                     'SELECT' => '',
00299                     'FROM' => '',
00300                     'WHERE' => '',
00301                     'GROUPBY' => '',
00302                     'ORDERBY' => '',
00303                     'LIMIT' => '',
00304                 );
00305 
00306                     // Find tables / field names to select:
00307                 $fieldArray = array();
00308                 $tableArray = array();
00309                 foreach($tables as $key => $val)    {
00310                     $tableArray[] = $key;
00311                     $resultfields = $tables[$key]['resultfields'];
00312                     if (is_array($resultfields))    {
00313                         foreach($resultfields as $key2 => $val2)    {
00314                             $fieldArray[] = $key.'.'.$val2;
00315                         }
00316                     }
00317                 }
00318                 $this->queryParts['SELECT'] = implode(',',$fieldArray);
00319                 $this->queryParts['FROM'] = implode(',',$tableArray);
00320 
00321                     // Set join WHERE parts:
00322                 $whereArray = array();
00323 
00324                 $primary_table_and_key = $primary_table.'.'.$tables[$primary_table]['primary_key'];
00325                 $primKeys = Array();
00326                 foreach($tables as $key => $val)    {
00327                     $fkey = $tables[$key]['fkey'];
00328                     if ($fkey)  {
00329                          $primKeys[] = $key.'.'.$fkey.'='.$primary_table_and_key;
00330                     }
00331                 }
00332                 if (count($primKeys))   {
00333                     $whereArray[] = '('.implode(' OR ',$primKeys).')';
00334                 }
00335 
00336                     // Additional where clause:
00337                 if (trim($endClause))   {
00338                     $whereArray[] = trim($endClause);
00339                 }
00340 
00341                     // Add search word where clause:
00342                 $query_part = $this->build_search_query_for_searchwords();
00343                 if (!$query_part)   {
00344                     $query_part = '(0!=0)';
00345                 }
00346                 $whereArray[] = '('.$query_part.')';
00347 
00348                     // Implode where clauses:
00349                 $this->queryParts['WHERE'] = implode(' AND ',$whereArray);
00350 
00351                     // Group by settings:
00352                 if ($this->group_by)    {
00353                     if ($this->group_by == 'PRIMARY_KEY')   {
00354                         $this->queryParts['GROUPBY'] = $primary_table_and_key;
00355                     } else {
00356                         $this->queryParts['GROUPBY'] = $this->group_by;
00357                     }
00358                 }
00359             }
00360         }
00361     }
00362 
00363     /**
00364      * Creates the part of the SQL-sentence, that searches for the search-words ($this->sword_array)
00365      *
00366      * @return  string      Part of where class limiting result to the those having the search word.
00367      * @access private
00368      */
00369     function build_search_query_for_searchwords()   {
00370 
00371         if (is_array($this->sword_array))   {
00372             $main_query_part = array();
00373 
00374             foreach($this->sword_array as $key => $val) {
00375                 $s_sword = $this->sword_array[$key]['sword'];
00376 
00377                     // Get subQueryPart
00378                 $sub_query_part = array();
00379 
00380                 $this->listOfSearchFields='';
00381                 foreach($this->tables as $key3 => $val3)    {
00382                     $searchfields = $this->tables[$key3]['searchfields'];
00383                     if (is_array($searchfields))    {
00384                         foreach($searchfields as $key2 => $val2)    {
00385                             $this->listOfSearchFields.= $key3.'.'.$val2.',';
00386                             $sub_query_part[] = $key3.'.'.$val2.' LIKE \'%'.$GLOBALS['TYPO3_DB']->quoteStr($s_sword, $key3).'%\'';
00387                         }
00388                     }
00389                 }
00390 
00391                 if (count($sub_query_part)) {
00392                     $main_query_part[] = $this->sword_array[$key]['oper'];
00393                     $main_query_part[] = '('.implode(' OR ',$sub_query_part).')';
00394                 }
00395             }
00396 
00397             if (count($main_query_part))    {
00398                 unset($main_query_part[0]); // Remove first part anyways.
00399                 return implode(' ',$main_query_part);
00400             }
00401         }
00402     }
00403 
00404     /**
00405      * This returns an SQL search-operator (eg. AND, OR, NOT) translated from the current localized set of operators (eg. in danish OG, ELLER, IKKE).
00406      *
00407      * @param   string      The possible operator to find in the internal operator array.
00408      * @return  string      If found, the SQL operator for the localized input operator.
00409      * @access private
00410      */
00411     function get_operator($operator)    {
00412         $operator = trim($operator);
00413         $op_array = $this->operator_translate_table;
00414         if ($this->operator_translate_table_caseinsensitive)    {
00415             $operator = strtolower($operator);  // case-conversion is charset insensitive, but it doesn't spoil anything if input string AND operator table is already converted
00416         }
00417         foreach ($op_array as $key => $val) {
00418             $item = $op_array[$key][0];
00419             if ($this->operator_translate_table_caseinsensitive)    {
00420                 $item = strtolower($item);  // See note above.
00421             }
00422             if ($operator==$item)   {
00423                 return $op_array[$key][1];
00424             }
00425         }
00426     }
00427 
00428     /**
00429      * Counts the results and sets the result in $this->res_count
00430      *
00431      * @return  boolean     True, if $this->query was found
00432      */
00433     function count_query() {
00434         if (is_array($this->queryParts))    {
00435             $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery($this->queryParts['SELECT'], $this->queryParts['FROM'], $this->queryParts['WHERE'], $this->queryParts['GROUPBY']);
00436             $this->res_count = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
00437             return TRUE;
00438         }
00439     }
00440 
00441     /**
00442      * Executes the search, sets result pointer in $this->result
00443      *
00444      * @return  boolean     True, if $this->query was set and query performed
00445      */
00446     function execute_query() {
00447         if (is_array($this->queryParts))    {
00448             $this->result = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($this->queryParts);
00449             return TRUE;
00450         }
00451     }
00452 
00453     /**
00454      * Returns URL-parameters with the current search words.
00455      * Used when linking to result pages so that search words can be highlighted.
00456      *
00457      * @return  string      URL-parameters with the searchwords
00458      */
00459     function get_searchwords()  {
00460         $SWORD_PARAMS = '';
00461         if (is_array($this->sword_array))   {
00462             foreach($this->sword_array as $key => $val) {
00463                 $SWORD_PARAMS.= '&sword_list[]='.rawurlencode($val['sword']);
00464             }
00465         }
00466         return $SWORD_PARAMS;
00467     }
00468 
00469     /**
00470      * Returns an array with the search words in
00471      *
00472      * @return  array       IF the internal sword_array contained search words it will return these, otherwise "void"
00473      */
00474     function get_searchwordsArray() {
00475         if (is_array($this->sword_array))   {
00476             foreach($this->sword_array as $key => $val) {
00477                 $swords[] = $val['sword'];
00478             }
00479         }
00480         return $swords;
00481     }
00482 }
00483 
00484 
00485 
00486 
00487 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['tslib/class.tslib_search.php'])) {
00488     include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['tslib/class.tslib_search.php']);
00489 }
00490 
00491 ?>