1 | <?php |
||
2 | |||
3 | /* |
||
4 | * @copyright 2014 Mautic Contributors. All rights reserved |
||
5 | * @author Mautic |
||
6 | * |
||
7 | * @link http://mautic.org |
||
8 | * |
||
9 | * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html |
||
10 | */ |
||
11 | |||
12 | namespace Mautic\CoreBundle\Helper; |
||
13 | |||
14 | /** |
||
15 | * Class SearchStringHelper. |
||
16 | */ |
||
17 | class SearchStringHelper |
||
18 | { |
||
19 | const COMMAND_NEGATE = 0; |
||
20 | const COMMAND_POSIT = 1; |
||
21 | const COMMAND_NEUTRAL = 2; |
||
22 | |||
23 | /** |
||
24 | * @var array |
||
25 | */ |
||
26 | protected $needsParsing = [ |
||
27 | ' ', |
||
28 | '(', |
||
29 | ')', |
||
30 | ]; |
||
31 | |||
32 | /** |
||
33 | * @var array |
||
34 | */ |
||
35 | protected $needsClosing = [ |
||
36 | 'quote' => '"', |
||
37 | 'parenthesis' => '(', |
||
38 | ]; |
||
39 | |||
40 | /** |
||
41 | * @var array |
||
42 | */ |
||
43 | protected $closingChars = [ |
||
44 | 'quote' => '"', |
||
45 | 'parenthesis' => ')', |
||
46 | ]; |
||
47 | |||
48 | /** |
||
49 | * SearchStringHelper constructor. |
||
50 | * |
||
51 | * @param array $needsParsing |
||
52 | * @param array $needsClosing |
||
53 | * @param array $closingChars |
||
54 | */ |
||
55 | public function __construct(array $needsParsing = null, array $needsClosing = null, array $closingChars = null) |
||
56 | { |
||
57 | if (null !== $needsParsing) { |
||
58 | $this->needsParsing = $needsParsing; |
||
59 | } |
||
60 | |||
61 | if (null !== $needsClosing) { |
||
62 | $this->needsClosing = $needsClosing; |
||
63 | } |
||
64 | |||
65 | if (null !== $closingChars) { |
||
66 | $this->closingChars = $closingChars; |
||
67 | } |
||
68 | } |
||
69 | |||
70 | /** |
||
71 | * @param string $input |
||
72 | * @param array $needsParsing |
||
73 | * @param array $needsClosing |
||
74 | * @param array $closingChars |
||
75 | * |
||
76 | * @return \stdClass |
||
77 | */ |
||
78 | public static function parseSearchString($input, array $needsParsing = null, array $needsClosing = null, array $closingChars = null) |
||
79 | { |
||
80 | $input = trim(strip_tags($input)); |
||
81 | |||
82 | $self = new self($needsParsing, $needsClosing, $closingChars); |
||
83 | |||
84 | return $self->parseString($input); |
||
85 | } |
||
86 | |||
87 | /** |
||
88 | * @param $input |
||
89 | */ |
||
90 | public function parseString($input) |
||
91 | { |
||
92 | return $this->splitUpSearchString($input); |
||
93 | } |
||
94 | |||
95 | /** |
||
96 | * @param $filters |
||
97 | */ |
||
98 | public static function mergeCommands(&$filters, array $commands) |
||
99 | { |
||
100 | if (!isset($filters->commands)) { |
||
101 | $filters->commands = $commands; |
||
102 | |||
103 | return; |
||
104 | } |
||
105 | |||
106 | foreach ($commands as $command => $status) { |
||
107 | if (isset($filters->commands[$command])) { |
||
108 | if ($status !== $filters->commands[$command]) { |
||
109 | $filters->commands[$command] = self::COMMAND_NEUTRAL; |
||
110 | } |
||
111 | } else { |
||
112 | $filters->commands[$command] = $status; |
||
113 | } |
||
114 | } |
||
115 | } |
||
116 | |||
117 | /** |
||
118 | * @param $filters |
||
119 | * @param $mergeFilter |
||
120 | */ |
||
121 | protected function addFilterCommand(&$filters, $mergeFilter) |
||
122 | { |
||
123 | $command = $mergeFilter->command; |
||
124 | if ('is' === $command) { |
||
125 | // Special case |
||
126 | $command = $command.':'.$mergeFilter->string; |
||
127 | } |
||
128 | if (!empty($command)) { |
||
129 | if (!isset($filters->commands[$command])) { |
||
130 | $filters->commands[$command] = ($mergeFilter->not) ? self::COMMAND_NEGATE : self::COMMAND_POSIT; |
||
131 | } else { |
||
132 | if (($mergeFilter->not && self::COMMAND_POSIT === $filters->commands[$command]) || !$mergeFilter->not && self::COMMAND_NEGATE === $filters->commands[$command]) { |
||
133 | $filters->commands[$command] = self::COMMAND_NEUTRAL; |
||
134 | } |
||
135 | } |
||
136 | } |
||
137 | } |
||
138 | |||
139 | /** |
||
140 | * @param string $input |
||
141 | * @param string $baseName |
||
142 | * @param string $overrideCommand |
||
143 | * |
||
144 | * @return \stdClass |
||
145 | */ |
||
146 | protected function splitUpSearchString($input, $baseName = 'root', $overrideCommand = '') |
||
147 | { |
||
148 | $keyCount = 0; |
||
149 | $command = $overrideCommand; |
||
150 | $filters = new \stdClass(); |
||
151 | $filters->commands = []; |
||
152 | $filters->{$baseName} = []; |
||
153 | $filters->{$baseName}[$keyCount] = new \stdClass(); |
||
154 | $filters->{$baseName}[$keyCount]->type = 'and'; |
||
155 | $filters->{$baseName}[$keyCount]->command = $command; |
||
156 | $filters->{$baseName}[$keyCount]->string = ''; |
||
157 | $filters->{$baseName}[$keyCount]->not = 0; |
||
158 | $filters->{$baseName}[$keyCount]->strict = 0; |
||
159 | $chars = str_split($input); |
||
160 | $pos = 0; |
||
161 | $string = ''; |
||
162 | |||
163 | //Iterate through every character to ensure that the search string is properly parsed from left to right while |
||
164 | //considering quotes, parenthesis, and commands |
||
165 | while (count($chars)) { |
||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
166 | $char = $chars[$pos]; |
||
167 | |||
168 | $string .= $char; |
||
169 | unset($chars[$pos]); |
||
170 | ++$pos; |
||
171 | |||
172 | if (':' == $char) { |
||
173 | //the string is a command |
||
174 | $command = trim(substr($string, 0, -1)); |
||
175 | //does this have a negative? |
||
176 | if (0 === strpos($command, '!')) { |
||
177 | $filters->{$baseName}[$keyCount]->not = 1; |
||
178 | $command = substr($command, 1); |
||
179 | } |
||
180 | |||
181 | if (empty($chars)) { |
||
182 | // Command hasn't been defined so don't allow empty or could end up searching entire table |
||
183 | unset($filters->{$baseName}[$keyCount]); |
||
184 | } else { |
||
185 | $filters->{$baseName}[$keyCount]->command = $command; |
||
186 | $string = ''; |
||
187 | } |
||
188 | } elseif (' ' == $char) { |
||
189 | //arrived at the end of a single word that is not within a quote or parenthesis so add it as standalone |
||
190 | if (' ' != $string) { |
||
191 | $string = trim($string); |
||
192 | $type = ('or' == strtolower($string) || 'and' == strtolower($string)) ? $string : ''; |
||
193 | $this->setFilter($filters, $baseName, $keyCount, $string, $command, $overrideCommand, true, $type, (!empty($chars))); |
||
194 | } |
||
195 | continue; |
||
196 | } elseif (in_array($char, $this->needsClosing)) { |
||
197 | //arrived at a character that has a closing partner and thus needs to be parsed as a group |
||
198 | |||
199 | //find the closing match |
||
200 | $key = array_search($char, $this->needsClosing); |
||
201 | |||
202 | $openingCount = 1; |
||
203 | $closingCount = 1; |
||
204 | |||
205 | //reiterate through the rest of the chars to find its closing match |
||
206 | foreach ($chars as $k => $c) { |
||
207 | $string .= $c; |
||
208 | unset($chars[$k]); |
||
209 | ++$pos; |
||
210 | |||
211 | if ($c === $this->closingChars[$key] && $openingCount === $closingCount) { |
||
212 | //found the matching character (accounts for nesting) |
||
213 | |||
214 | //remove wrapping grouping chars |
||
215 | if (0 === strpos($string, $char) && substr($string, -1) === $c) { |
||
216 | $string = substr($string, 1, -1); |
||
217 | } |
||
218 | |||
219 | //handle characters that support nesting |
||
220 | $neededParsing = false; |
||
221 | if ('"' !== $c) { |
||
222 | //check to see if the nested string needs to be parsed as well |
||
223 | foreach ($this->needsParsing as $parseMe) { |
||
224 | if (false !== strpos($string, $parseMe)) { |
||
225 | $parsed = $this->splitUpSearchString($string, 'parsed', $command); |
||
226 | $filters->{$baseName}[$keyCount]->children = $parsed->parsed; |
||
227 | $neededParsing = true; |
||
228 | break; |
||
229 | } |
||
230 | } |
||
231 | } |
||
232 | |||
233 | $this->setFilter($filters, $baseName, $keyCount, $string, $command, $overrideCommand, (!$neededParsing)); |
||
234 | |||
235 | break; |
||
236 | } elseif ($c === $char) { |
||
237 | //this is another opening char so keep track of it to properly handle nested strings |
||
238 | ++$openingCount; |
||
239 | } elseif ($c === $this->closingChars[$key]) { |
||
240 | //this is a closing char within a nest but not the one to close the group |
||
241 | ++$closingCount; |
||
242 | } |
||
243 | } |
||
244 | } elseif (empty($chars)) { |
||
245 | $filters->{$baseName}[$keyCount]->command = $command; |
||
246 | $this->setFilter($filters, $baseName, $keyCount, $string, $command, $overrideCommand, true, null, false); |
||
247 | }//else keep concocting chars |
||
248 | } |
||
249 | |||
250 | return $filters; |
||
251 | } |
||
252 | |||
253 | private function setFilter(&$filters, &$baseName, &$keyCount, &$string, &$command, $overrideCommand, |
||
254 | $setFilter = true, |
||
255 | $type = null, |
||
256 | $setUpNext = true) |
||
257 | { |
||
258 | if (!empty($type)) { |
||
259 | $filters->{$baseName}[$keyCount]->type = strtolower($type); |
||
260 | } elseif ($setFilter) { |
||
261 | $string = trim(strtolower($string)); |
||
262 | |||
263 | //remove operators and empty values |
||
264 | if (in_array($string, ['', 'or', 'and'])) { |
||
265 | unset($filters->{$baseName}[$keyCount]); |
||
266 | |||
267 | return; |
||
268 | } |
||
269 | |||
270 | if (!isset($filters->{$baseName}[$keyCount]->strict)) { |
||
271 | $filters->{$baseName}[$keyCount]->strict = 0; |
||
272 | } |
||
273 | if (!isset($filters->{$baseName}[$keyCount]->not)) { |
||
274 | $filters->{$baseName}[$keyCount]->not = 0; |
||
275 | } |
||
276 | |||
277 | $strictPos = strpos($string, '+'); |
||
278 | $notPos = strpos($string, '!'); |
||
279 | if ((0 === $strictPos || 1 === $strictPos || 0 === $notPos || 1 === $notPos)) { |
||
280 | if (false !== $strictPos && false !== $notPos) { |
||
281 | //+! or !+ |
||
282 | $filters->{$baseName}[$keyCount]->strict = 1; |
||
283 | $filters->{$baseName}[$keyCount]->not = 1; |
||
284 | $string = substr($string, 2); |
||
285 | } elseif (0 === $strictPos && false === $notPos) { |
||
286 | //+ |
||
287 | $filters->{$baseName}[$keyCount]->strict = 1; |
||
288 | $filters->{$baseName}[$keyCount]->not = 0; |
||
289 | $string = substr($string, 1); |
||
290 | } elseif (false === $strictPos && 0 === $notPos) { |
||
291 | //! |
||
292 | $filters->{$baseName}[$keyCount]->strict = 0; |
||
293 | $filters->{$baseName}[$keyCount]->not = 1; |
||
294 | $string = substr($string, 1); |
||
295 | } |
||
296 | } |
||
297 | |||
298 | $filters->{$baseName}[$keyCount]->string = $string; |
||
299 | |||
300 | $this->addFilterCommand($filters, $filters->{$baseName}[$keyCount]); |
||
301 | |||
302 | //setup the next filter |
||
303 | if ($setUpNext) { |
||
304 | ++$keyCount; |
||
305 | $filters->{$baseName}[$keyCount] = new \stdClass(); |
||
306 | $filters->{$baseName}[$keyCount]->type = 'and'; |
||
307 | $filters->{$baseName}[$keyCount]->command = $overrideCommand; |
||
308 | $filters->{$baseName}[$keyCount]->string = ''; |
||
309 | $filters->{$baseName}[$keyCount]->not = 0; |
||
310 | $filters->{$baseName}[$keyCount]->strict = 0; |
||
311 | } |
||
312 | } |
||
313 | $string = ''; |
||
314 | $command = $overrideCommand; |
||
315 | } |
||
316 | } |
||
317 |