Passed
Pull Request — master (#483)
by
unknown
02:48
created

Expression::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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