Issues (3627)

bundles/CoreBundle/Helper/SearchStringHelper.php (1 issue)

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
It seems like $chars can also be of type true; however, parameter $value of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

165
        while (count(/** @scrutinizer ignore-type */ $chars)) {
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