1<?php
2/*
3    +-----------------------------------------------------------------------------+
4    | ILIAS open source                                                           |
5    +-----------------------------------------------------------------------------+
6    | Copyright (c) 1998-2001 ILIAS open source, University of Cologne            |
7    |                                                                             |
8    | This program is free software; you can redistribute it and/or               |
9    | modify it under the terms of the GNU General Public License                 |
10    | as published by the Free Software Foundation; either version 2              |
11    | of the License, or (at your option) any later version.                      |
12    |                                                                             |
13    | This program is distributed in the hope that it will be useful,             |
14    | but WITHOUT ANY WARRANTY; without even the implied warranty of              |
15    | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               |
16    | GNU General Public License for more details.                                |
17    |                                                                             |
18    | You should have received a copy of the GNU General Public License           |
19    | along with this program; if not, write to the Free Software                 |
20    | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. |
21    +-----------------------------------------------------------------------------+
22*/
23
24/**
25* Class ilQueryParser
26*
27* Class for parsing search queries. An instance of this object is required for every Search class (MetaSearch ...)
28*
29* @author Stefan Meyer <meyer@leifos.com>
30* @version $Id$
31*
32* @package ilias-search
33*
34*/
35define('QP_COMBINATION_AND', 'and');
36define('QP_COMBINATION_OR', 'or');
37
38class ilQueryParser
39{
40    /**
41     * Minimum of characters required for search
42     */
43    const MIN_WORD_LENGTH = 3;
44
45    public $lng = null;
46
47    public $min_word_length = 0;
48    public $global_min_length = null;
49
50    public $query_str;
51    public $quoted_words = array();
52    public $message; // Translated error message
53    public $combination; // combiniation of search words e.g 'and' or 'or'
54    protected $settings = null;
55    protected $wildcards_allowed; // [bool]
56
57    /**
58    * Constructor
59    * @access public
60    */
61    public function __construct($a_query_str)
62    {
63        global $DIC;
64
65        $lng = $DIC['lng'];
66
67        define('MIN_WORD_LENGTH', self::MIN_WORD_LENGTH);
68
69        $this->lng = $lng;
70
71        $this->query_str = $a_query_str;
72        $this->message = '';
73
74        include_once './Services/Search/classes/class.ilSearchSettings.php';
75        $this->settings = ilSearchSettings::getInstance();
76
77        if (!$this->setMinWordLength(1)) {
78            $this->setMinWordLength(MIN_WORD_LENGTH);
79        }
80
81        $this->setAllowedWildcards(false);
82    }
83
84    public function setMinWordLength($a_length, $a_force = false)
85    {
86        // Due to a bug in mysql fulltext search queries with min_word_legth < 3
87        // might freeze the system.
88        // Thus min_word_length cannot be set to values < 3 if the mysql fulltext is used.
89        if (!$a_force and $this->settings->enabledIndex() and $a_length < 3) {
90            ilLoggerFactory::getLogger('src')->debug('Disabled min word length');
91            return false;
92        }
93        $this->min_word_length = $a_length;
94        return true;
95    }
96    public function getMinWordLength()
97    {
98        return $this->min_word_length;
99    }
100
101    public function setGlobalMinLength($a_value)
102    {
103        if ($a_value !== null) {
104            $a_value = (int) $a_value;
105            if ($a_value < 1) {
106                return;
107            }
108        }
109        $this->global_min_length = $a_value;
110    }
111
112    public function getGlobalMinLength()
113    {
114        return $this->global_min_length;
115    }
116
117    public function setAllowedWildcards($a_value)
118    {
119        $this->wildcards_allowed = (bool) $a_value;
120    }
121
122    public function getAllowedWildcards()
123    {
124        return $this->wildcards_allowed;
125    }
126
127    public function setMessage($a_msg)
128    {
129        $this->message = $a_msg;
130    }
131    public function getMessage()
132    {
133        return $this->message;
134    }
135    public function appendMessage($a_msg)
136    {
137        if (strlen($this->getMessage())) {
138            $this->message .= '<br />';
139        }
140        $this->message .= $a_msg;
141    }
142
143    public function setCombination($a_combination)
144    {
145        $this->combination = $a_combination;
146    }
147    public function getCombination()
148    {
149        return $this->combination;
150    }
151
152    public function getQueryString()
153    {
154        return trim($this->query_str);
155    }
156    public function getWords()
157    {
158        return $this->words ? $this->words : array();
159    }
160
161    public function getQuotedWords($with_quotation = false)
162    {
163        if ($with_quotation) {
164            return $this->quoted_words ? $this->quoted_words : array();
165        } else {
166            foreach ($this->quoted_words as $word) {
167                $tmp_word[] = str_replace('\"', '', $word);
168            }
169            return $tmp_word ? $tmp_word : array();
170        }
171    }
172
173    public function getLuceneQueryString()
174    {
175        $counter = 0;
176        $tmp_str = "";
177        foreach ($this->getQuotedWords(true) as $word) {
178            if ($counter++) {
179                $tmp_str .= (" " . strtoupper($this->getCombination()) . " ");
180            }
181            $tmp_str .= $word;
182        }
183        return $tmp_str;
184    }
185    public function parse()
186    {
187        $this->words = array();
188
189        #if(!strlen($this->getQueryString()))
190        #{
191        #	return false;
192        #}
193
194        $words = explode(' ', trim($this->getQueryString()));
195        foreach ($words as $word) {
196            if (!strlen(trim($word))) {
197                continue;
198            }
199
200            if (strlen(trim($word)) < $this->getMinWordLength()) {
201                $this->setMessage(sprintf($this->lng->txt('search_minimum_info'), $this->getMinWordLength()));
202                continue;
203            }
204
205            $this->words[] = ilUtil::prepareDBString($word);
206        }
207
208        $fullstr = trim($this->getQueryString());
209        if (!in_array($fullstr, $this->words)) {
210            $this->words[] = ilUtil::prepareDBString($fullstr);
211        }
212
213        if (!$this->getAllowedWildcards()) {
214            // #14768
215            foreach ($this->words as $idx => $word) {
216                if (!stristr($word, '\\')) {
217                    $word = str_replace('%', '\%', $word);
218                    $word = str_replace('_', '\_', $word);
219                }
220                $this->words[$idx] = $word;
221            }
222        }
223
224        // Parse strings like && 'A "B C D" E' as 'A' && 'B C D' && 'E'
225        // Can be used in LIKE search or fulltext search > MYSQL 4.0
226        $this->__parseQuotation();
227
228        return true;
229    }
230
231    public function __parseQuotation()
232    {
233        if (!strlen($this->getQueryString())) {
234            $this->quoted_words[] = '';
235            return false;
236        }
237        $query_str = $this->getQueryString();
238        while (preg_match("/\".*?\"/", $query_str, $matches)) {
239            $query_str = str_replace($matches[0], '', $query_str);
240            $this->quoted_words[] = ilUtil::prepareDBString($matches[0]);
241        }
242
243        // Parse the rest
244        $words = explode(' ', trim($query_str));
245        foreach ($words as $word) {
246            if (!strlen(trim($word))) {
247                continue;
248            }
249
250            $this->quoted_words[] = ilUtil::prepareDBString($word);
251        }
252
253        if (!$this->getAllowedWildcards()) {
254            // #14768
255            foreach ($this->quoted_words as $idx => $word) {
256                if (!stristr($word, '\\')) {
257                    $word = str_replace('%', '\%', $word);
258                    $word = str_replace('_', '\_', $word);
259                }
260                $this->quoted_words[$idx] = $word;
261            }
262        }
263    }
264
265    public function validate()
266    {
267        // Words with less than 3 characters
268        if (strlen($this->getMessage())) {
269            return false;
270        }
271        // No search string given
272        if ($this->getMinWordLength() and !count($this->getWords())) {
273            $this->setMessage($this->lng->txt('msg_no_search_string'));
274            return false;
275        }
276        // No search string given
277        if ($this->getGlobalMinLength() and strlen(str_replace('"', '', $this->getQueryString())) < $this->getGlobalMinLength()) {
278            $this->setMessage(sprintf($this->lng->txt('search_minimum_info'), $this->getGlobalMinLength()));
279            return false;
280        }
281
282        return true;
283    }
284}
285