Passed
Push — master ( 2d7662...9d3cd8 )
by
unknown
02:52
created

Expression::__construct()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 2
nop 4
dl 0
loc 10
ccs 8
cts 8
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Components;
6
7
use PhpMyAdmin\SqlParser\Component;
8
use PhpMyAdmin\SqlParser\Context;
9
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
10
use PhpMyAdmin\SqlParser\Parser;
11
use PhpMyAdmin\SqlParser\Token;
12
use PhpMyAdmin\SqlParser\TokensList;
13
use PhpMyAdmin\SqlParser\TokenType;
14
15
use function implode;
16
use function rtrim;
17
use function strlen;
18
use function trim;
19
20
/**
21
 * Parses a reference to an expression (column, table or database name, function
22
 * call, mathematical expression, etc.).
23
 */
24
#[\AllowDynamicProperties]
25
final class Expression implements Component
26
{
27
    /**
28
     * List of allowed reserved keywords in expressions.
29
     */
30
    private const ALLOWED_KEYWORDS = [
31
        'AND' => 1,
32
        'AS' => 1,
33
        'BETWEEN' => 1,
34
        'CASE' => 1,
35
        'DUAL' => 1,
36
        'DIV' => 1,
37
        'IS' => 1,
38
        'MOD' => 1,
39
        'NOT' => 1,
40
        'NOT NULL' => 1,
41
        'NULL' => 1,
42
        'OR' => 1,
43
        'OVER' => 1,
44
        'REGEXP' => 1,
45
        'RLIKE' => 1,
46
        'XOR' => 1,
47
    ];
48
49
    /**
50
     * The name of this database.
51
     *
52
     * @var string|null
53
     */
54
    public $database;
55
56
    /**
57
     * The name of this table.
58
     *
59
     * @var string|null
60
     */
61
    public $table;
62
63
    /**
64
     * The name of the column.
65
     *
66
     * @var string|null
67
     */
68
    public $column;
69
70
    /**
71
     * The sub-expression.
72
     *
73
     * @var string|null
74
     */
75
    public $expr = '';
76
77
    /**
78
     * The alias of this expression.
79
     *
80
     * @var string|null
81
     */
82
    public $alias;
83
84
    /**
85
     * The name of the function.
86
     *
87
     * @var mixed
88
     */
89
    public $function;
90
91
    /**
92
     * The type of subquery.
93
     *
94
     * @var string|null
95
     */
96
    public $subquery;
97
98
    /**
99
     * Syntax:
100
     *     new Expression('expr')
101
     *     new Expression('expr', 'alias')
102
     *     new Expression('database', 'table', 'column')
103
     *     new Expression('database', 'table', 'column', 'alias')
104
     *
105
     * If the database, table or column name is not required, pass an empty
106
     * string.
107
     *
108
     * @param string|null $database The name of the database or the expression.
109
     * @param string|null $table    The name of the table or the alias of the expression.
110
     * @param string|null $column   the name of the column
111
     * @param string|null $alias    the name of the alias
112
     */
113 1114
    public function __construct($database = null, $table = null, $column = null, $alias = null)
114
    {
115 1114
        if (($column === null) && ($alias === null)) {
116 1114
            $this->expr = $database; // case 1
117 1114
            $this->alias = $table; // case 2
118
        } else {
119 4
            $this->database = $database; // case 3
120 4
            $this->table = $table; // case 3
121 4
            $this->column = $column; // case 3
122 4
            $this->alias = $alias; // case 4
123
        }
124
    }
125
126
    /**
127
     * Possible options:.
128
     *
129
     *      `field`
130
     *
131
     *          First field to be filled.
132
     *          If this is not specified, it takes the value of `parseField`.
133
     *
134
     *      `parseField`
135
     *
136
     *          Specifies the type of the field parsed. It may be `database`,
137
     *          `table` or `column`. These expressions may not include
138
     *          parentheses.
139
     *
140
     *      `breakOnAlias`
141
     *
142
     *          If not empty, breaks when the alias occurs (it is not included).
143
     *
144
     *      `breakOnParentheses`
145
     *
146
     *          If not empty, breaks when the first parentheses occurs.
147
     *
148
     *      `parenthesesDelimited`
149
     *
150
     *          If not empty, breaks after last parentheses occurred.
151
     *
152
     * @param Parser               $parser  the parser that serves as context
153
     * @param TokensList           $list    the list of tokens that are being parsed
154
     * @param array<string, mixed> $options parameters for parsing
155
     *
156
     * @throws ParserException
157
     */
158 1104
    public static function parse(Parser $parser, TokensList $list, array $options = []): Expression|null
159
    {
160 1104
        $ret = new static();
161
162
        /**
163
         * Whether current tokens make an expression or a table reference.
164
         *
165
         * @var bool
166
         */
167 1104
        $isExpr = false;
168
169
        /**
170
         * Whether a period was previously found.
171
         *
172
         * @var bool
173
         */
174 1104
        $dot = false;
175
176
        /**
177
         * Whether an alias is expected. Is 2 if `AS` keyword was found.
178
         *
179
         * @var bool
180
         */
181 1104
        $alias = false;
182
183
        /**
184
         * Counts brackets.
185
         *
186
         * @var int
187
         */
188 1104
        $brackets = 0;
189
190
        /**
191
         * Keeps track of the last two previous tokens.
192
         *
193
         * @var Token[]
194
         */
195 1104
        $prev = [
196 1104
            null,
197 1104
            null,
198 1104
        ];
199
200
        // When a field is parsed, no parentheses are expected.
201 1104
        if (! empty($options['parseField'])) {
202 636
            $options['breakOnParentheses'] = true;
203 636
            $options['field'] = $options['parseField'];
204
        }
205
206 1104
        for (; $list->idx < $list->count; ++$list->idx) {
207
            /**
208
             * Token parsed at this moment.
209
             */
210 1104
            $token = $list->tokens[$list->idx];
211
212
            // End of statement.
213 1104
            if ($token->type === TokenType::Delimiter) {
214 428
                break;
215
            }
216
217
            // Skipping whitespaces and comments.
218 1102
            if (($token->type === TokenType::Whitespace) || ($token->type === TokenType::Comment)) {
219
                // If the token is a closing C comment from a MySQL command, it must be ignored.
220 942
                if ($isExpr && $token->token !== '*/') {
221 410
                    $ret->expr .= $token->token;
222
                }
223
224 942
                continue;
225
            }
226
227 1102
            if ($token->type === TokenType::Keyword) {
228 860
                if (($brackets > 0) && empty($ret->subquery) && ! empty(Parser::STATEMENT_PARSERS[$token->keyword])) {
229
                    // A `(` was previously found and this keyword is the
230
                    // beginning of a statement, so this is a subquery.
231 68
                    $ret->subquery = $token->keyword;
232
                } elseif (
233 858
                    ($token->flags & Token::FLAG_KEYWORD_FUNCTION)
234 858
                    && (empty($options['parseField'])
235 858
                    && ! $alias)
236
                ) {
237 104
                    $isExpr = true;
238 834
                } elseif (($token->flags & Token::FLAG_KEYWORD_RESERVED) && ($brackets === 0)) {
239 772
                    if (empty(self::ALLOWED_KEYWORDS[$token->keyword])) {
240
                        // A reserved keyword that is not allowed in the
241
                        // expression was found so the expression must have
242
                        // ended and a new clause is starting.
243 724
                        break;
244
                    }
245
246 204
                    if ($token->keyword === 'AS') {
247 156
                        if (! empty($options['breakOnAlias'])) {
248 30
                            break;
249
                        }
250
251 142
                        if ($alias) {
252 2
                            $parser->error('An alias was expected.', $token);
253 2
                            break;
254
                        }
255
256 142
                        $alias = true;
257 142
                        continue;
258
                    }
259
260 64
                    if ($token->keyword === 'CASE') {
261
                        // For a use of CASE like
262
                        // 'SELECT a = CASE .... END, b=1, `id`, ... FROM ...'
263 10
                        $tempCaseExpr = CaseExpression::parse($parser, $list);
264 10
                        $ret->expr .= $tempCaseExpr->build();
265 10
                        $isExpr = true;
266 10
                        continue;
267
                    }
268
269 54
                    $isExpr = true;
270 238
                } elseif ($brackets === 0 && strlen((string) $ret->expr) > 0 && ! $alias) {
271
                    /* End of expression */
272 164
                    break;
273
                }
274
            }
275
276
            if (
277 1098
                ($token->type === TokenType::Number)
278 1074
                || ($token->type === TokenType::Bool)
279 1074
                || (($token->type === TokenType::Symbol)
280 1074
                && ($token->flags & Token::FLAG_SYMBOL_VARIABLE))
281 1066
                || (($token->type === TokenType::Symbol)
282 1066
                && ($token->flags & Token::FLAG_SYMBOL_PARAMETER))
283 1098
                || (($token->type === TokenType::Operator)
284 1098
                && ($token->value !== '.'))
285
            ) {
286 704
                if (! empty($options['parseField'])) {
287 236
                    break;
288
                }
289
290
                // Numbers, booleans and operators (except dot) are usually part
291
                // of expressions.
292 550
                $isExpr = true;
293
            }
294
295 1098
            if ($token->type === TokenType::Operator) {
296 486
                if (! empty($options['breakOnParentheses']) && (($token->value === '(') || ($token->value === ')'))) {
297
                    // No brackets were expected.
298 4
                    break;
299
                }
300
301 484
                if ($token->value === '(') {
302 180
                    ++$brackets;
303
                    if (
304 180
                        empty($ret->function) && ($prev[1] !== null)
305 180
                        && (($prev[1]->type === TokenType::None)
306 180
                        || ($prev[1]->type === TokenType::Symbol)
307 180
                        || (($prev[1]->type === TokenType::Keyword)
308 180
                        && ($prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION)))
309
                    ) {
310 56
                        $ret->function = $prev[1]->value;
311
                    }
312 482
                } elseif ($token->value === ')') {
313 194
                    if ($brackets === 0) {
314
                        // Not our bracket
315 22
                        break;
316
                    }
317
318 178
                    --$brackets;
319 178
                    if ($brackets === 0) {
320 178
                        if (! empty($options['parenthesesDelimited'])) {
321
                            // The current token is the last bracket, the next
322
                            // one will be outside the expression.
323 50
                            $ret->expr .= $token->token;
324 50
                            ++$list->idx;
325 50
                            break;
326
                        }
327 22
                    } elseif ($brackets < 0) {
328
                        // $parser->error('Unexpected closing bracket.', $token);
329
                        // $brackets = 0;
330
                        break;
331
                    }
332 430
                } elseif ($token->value === ',') {
333
                    // Expressions are comma-delimited.
334 278
                    if ($brackets === 0) {
335 244
                        break;
336
                    }
337
                }
338
            }
339
340
            // Saving the previous tokens.
341 1096
            $prev[0] = $prev[1];
342 1096
            $prev[1] = $token;
343
344 1096
            if ($alias) {
345
                // An alias is expected (the keyword `AS` was previously found).
346 140
                if (! empty($ret->alias)) {
347 2
                    $parser->error('An alias was previously found.', $token);
348 2
                    break;
349
                }
350
351 140
                $ret->alias = $token->value;
352 140
                $alias = false;
353 1096
            } elseif ($isExpr) {
354
                // Handling aliases.
355
                if (
356 506
                    $brackets === 0
357 506
                    && ($prev[0] === null
358 506
                        || (($prev[0]->type !== TokenType::Operator || $prev[0]->token === ')')
359 506
                            && ($prev[0]->type !== TokenType::Keyword
360 506
                                || ! ($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED))))
361 506
                    && (($prev[1]->type === TokenType::String)
362 506
                        || ($prev[1]->type === TokenType::Symbol
363 506
                            && ! ($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE)
364 506
                            && ! ($prev[1]->flags & Token::FLAG_SYMBOL_PARAMETER))
365 506
                        || ($prev[1]->type === TokenType::None
366 506
                            && $prev[1]->token !== 'OVER'))
367
                ) {
368 14
                    if (! empty($ret->alias)) {
369 4
                        $parser->error('An alias was previously found.', $token);
370 4
                        break;
371
                    }
372
373 12
                    $ret->alias = $prev[1]->value;
374
                } else {
375 506
                    $currIdx = $list->idx;
376 506
                    --$list->idx;
377 506
                    $beforeToken = $list->getPrevious();
378 506
                    $list->idx = $currIdx;
379
                    // columns names tokens are of type NONE, or SYMBOL (`col`), and the columns options
380
                    // would start with a token of type KEYWORD, in that case, we want to have a space
381
                    // between the tokens.
382
                    if (
383 506
                        $ret->expr !== null &&
384
                        $beforeToken &&
385 506
                        ($beforeToken->type === TokenType::None ||
386 506
                        $beforeToken->type === TokenType::Symbol || $beforeToken->type === TokenType::String) &&
387 506
                        $token->type === TokenType::Keyword
388
                    ) {
389 76
                        $ret->expr = rtrim($ret->expr, ' ') . ' ';
390
                    }
391
392 506
                    $ret->expr .= $token->token;
393
                }
394
            } else {
395 1006
                if (($token->type === TokenType::Operator) && ($token->value === '.')) {
396
                    // Found a `.` which means we expect a column name and
397
                    // the column name we parsed is actually the table name
398
                    // and the table name is actually a database name.
399 112
                    if (! empty($ret->database) || $dot) {
400 4
                        $parser->error('Unexpected dot.', $token);
401
                    }
402
403 112
                    $ret->database = $ret->table;
404 112
                    $ret->table = $ret->column;
405 112
                    $ret->column = null;
406 112
                    $dot = true;
407 112
                    $ret->expr .= $token->token;
408
                } else {
409 1006
                    $field = empty($options['field']) ? 'column' : $options['field'];
410 1006
                    if (empty($ret->$field)) {
411 1006
                        $ret->$field = $token->value;
412 1006
                        $ret->expr .= $token->token;
413 1006
                        $dot = false;
414
                    } else {
415
                        // No alias is expected.
416 78
                        if (! empty($options['breakOnAlias'])) {
417 24
                            break;
418
                        }
419
420 54
                        if (! empty($ret->alias)) {
421 8
                            $parser->error('An alias was previously found.', $token);
422 8
                            break;
423
                        }
424
425 50
                        $ret->alias = $token->value;
426
                    }
427
                }
428
            }
429
        }
430
431 1104
        if ($alias) {
432 6
            $parser->error('An alias was expected.', $list->tokens[$list->idx - 1]);
433
        }
434
435
        // White-spaces might be added at the end.
436 1104
        $ret->expr = trim((string) $ret->expr);
437
438 1104
        if ($ret->expr === '') {
439 58
            return null;
440
        }
441
442 1096
        --$list->idx;
443
444 1096
        return $ret;
445
    }
446
447 260
    public function build(): string
448
    {
449 260
        if ($this->expr !== '' && $this->expr !== null) {
450 242
            $ret = $this->expr;
451
        } else {
452 22
            $fields = [];
453 22
            if (isset($this->database) && ($this->database !== '')) {
454 2
                $fields[] = $this->database;
455
            }
456
457 22
            if (isset($this->table) && ($this->table !== '')) {
458 22
                $fields[] = $this->table;
459
            }
460
461 22
            if (isset($this->column) && ($this->column !== '')) {
462 2
                $fields[] = $this->column;
463
            }
464
465 22
            $ret = implode('.', Context::escapeAll($fields));
466
        }
467
468 260
        if (! empty($this->alias)) {
469 32
            $ret .= ' AS ' . Context::escape($this->alias);
470
        }
471
472 260
        return $ret;
473
    }
474
475
    /**
476
     * @param Expression[] $component the component to be built
477
     */
478 14
    public static function buildAll(array $component): string
479
    {
480 14
        return implode(', ', $component);
481
    }
482
483 226
    public function __toString(): string
484
    {
485 226
        return $this->build();
486
    }
487
}
488