Test Failed
Pull Request — master (#291)
by William
12:25
created

Expression::__construct()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 9
cts 9
cp 1
rs 9.8666
c 0
b 0
f 0
cc 3
nc 2
nop 4
crap 3
1
<?php
2
3
/**
4
 * Parses a reference to an expression (column, table or database name, function
5
 * call, mathematical expression, etc.).
6
 */
7
8
namespace PhpMyAdmin\SqlParser\Components;
9
10
use PhpMyAdmin\SqlParser\Component;
11
use PhpMyAdmin\SqlParser\Context;
12
use PhpMyAdmin\SqlParser\Parser;
13
use PhpMyAdmin\SqlParser\Token;
14
use PhpMyAdmin\SqlParser\TokensList;
15
16
/**
17
 * Parses a reference to an expression (column, table or database name, function
18
 * call, mathematical expression, etc.).
19
 *
20
 * @category   Components
21
 *
22
 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
23
 */
24
class Expression extends Component
25
{
26
    /**
27
     * List of allowed reserved keywords in expressions.
28
     *
29
     * @var array
30
     */
31
    private static $ALLOWED_KEYWORDS = array(
32
        'AS' => 1,
33
        'DUAL' => 1,
34
        'NULL' => 1,
35
        'REGEXP' => 1,
36
        'CASE' => 1,
37
        'DIV' => 1,
38
        'AND' => 1,
39
        'OR' => 1,
40
        'XOR' => 1,
41
        'NOT' => 1,
42
        'MOD' => 1
43
    );
44
45
    /**
46
     * The name of this database.
47
     *
48
     * @var string
49
     */
50
    public $database;
51
52
    /**
53
     * The name of this table.
54
     *
55
     * @var string
56
     */
57
    public $table;
58
59
    /**
60
     * The name of the column.
61
     *
62
     * @var string
63
     */
64
    public $column;
65
66
    /**
67
     * The sub-expression.
68
     *
69
     * @var string
70
     */
71
    public $expr = '';
72
73
    /**
74
     * The alias of this expression.
75
     *
76
     * @var string
77
     */
78
    public $alias;
79
80
    /**
81
     * The name of the function.
82
     *
83
     * @var mixed
84
     */
85
    public $function;
86
87
    /**
88
     * The type of subquery.
89
     *
90
     * @var string
91
     */
92
    public $subquery;
93
94
    /**
95
     * Constructor.
96
     *
97
     * Syntax:
98
     *     new Expression('expr')
99
     *     new Expression('expr', 'alias')
100
     *     new Expression('database', 'table', 'column')
101
     *     new Expression('database', 'table', 'column', 'alias')
102
     *
103
     * If the database, table or column name is not required, pass an empty
104
     * string.
105
     *
106
     * @param string $database The name of the database or the the expression.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $database not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
107
     *                         the the expression.
108
     * @param string $table    The name of the table or the alias of the expression.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $table not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
109
     *                         the alias of the expression.
110
     * @param string $column   the name of the column
0 ignored issues
show
Documentation introduced by
Should the type for parameter $column not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
111
     * @param string $alias    the name of the alias
0 ignored issues
show
Documentation introduced by
Should the type for parameter $alias not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
112
     */
113 354
    public function __construct($database = null, $table = null, $column = null, $alias = null)
114
    {
115 354
        if (($column === null) && ($alias === null)) {
116 354
            $this->expr = $database; // case 1
117 354
            $this->alias = $table; // case 2
118
        } else {
119 2
            $this->database = $database; // case 3
120 2
            $this->table = $table; // case 3
121 2
            $this->column = $column; // case 3
122 2
            $this->alias = $alias; // case 4
123
        }
124 354
    }
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      $options parameters for parsing
155
     *
156
     * @return Expression|null
157
     * @throws \PhpMyAdmin\SqlParser\Exceptions\ParserException
158
     */
159 348
    public static function parse(Parser $parser, TokensList $list, array $options = array())
160
    {
161 348
        $ret = new self();
162
163
        /**
164
         * Whether current tokens make an expression or a table reference.
165
         *
166
         * @var bool
167
         */
168 348
        $isExpr = false;
169
170
        /**
171
         * Whether a period was previously found.
172
         *
173
         * @var bool
174
         */
175 348
        $dot = false;
176
177
        /**
178
         * Whether an alias is expected. Is 2 if `AS` keyword was found.
179
         *
180
         * @var bool
181
         */
182 348
        $alias = false;
183
184
        /**
185
         * Counts brackets.
186
         *
187
         * @var int
188
         */
189 348
        $brackets = 0;
190
191
        /**
192
         * Keeps track of the last two previous tokens.
193
         *
194
         * @var Token[]
195
         */
196
        $prev = array(
197 348
            null,
198
            null
199
        );
200
201
        // When a field is parsed, no parentheses are expected.
202 348
        if (! empty($options['parseField'])) {
203 176
            $options['breakOnParentheses'] = true;
204 176
            $options['field'] = $options['parseField'];
205
        }
206
207 348
        for (; $list->idx < $list->count; ++$list->idx) {
208
            /**
209
             * Token parsed at this moment.
210
             *
211
             * @var Token
212
             */
213 348
            $token = $list->tokens[$list->idx];
214
215
            // End of statement.
216 348
            if ($token->type === Token::TYPE_DELIMITER) {
217 135
                break;
218
            }
219
220
            // Skipping whitespaces and comments.
221 347
            if (($token->type === Token::TYPE_WHITESPACE)
222 347
                || ($token->type === Token::TYPE_COMMENT)
223
            ) {
224 284
                if ($isExpr) {
225 128
                    $ret->expr .= $token->token;
226
                }
227 284
                continue;
228
            }
229
230 347
            if ($token->type === Token::TYPE_KEYWORD) {
231 265
                if (($brackets > 0) && empty($ret->subquery)
232 265
                    && ! empty(Parser::$STATEMENT_PARSERS[$token->keyword])
233
                ) {
234
                    // A `(` was previously found and this keyword is the
235
                    // beginning of a statement, so this is a subquery.
236 29
                    $ret->subquery = $token->keyword;
237 264
                } elseif (($token->flags & Token::FLAG_KEYWORD_FUNCTION)
238 33
                    && (empty($options['parseField'])
239 264
                    && ! $alias)
240
                ) {
241 22
                    $isExpr = true;
242 258
                } elseif (($token->flags & Token::FLAG_KEYWORD_RESERVED)
243 258
                    && ($brackets === 0)
244
                ) {
245 248
                    if (empty(self::$ALLOWED_KEYWORDS[$token->keyword])) {
246
                        // A reserved keyword that is not allowed in the
247
                        // expression was found so the expression must have
248
                        // ended and a new clause is starting.
249 231
                        break;
250
                    }
251 76
                    if ($token->keyword === 'AS') {
252 62
                        if (! empty($options['breakOnAlias'])) {
253 5
                            break;
254
                        }
255 57
                        if ($alias) {
256 1
                            $parser->error(
257 1
                                'An alias was expected.',
258
                                $token
259
                            );
260 1
                            break;
261
                        }
262 57
                        $alias = true;
263 57
                        continue;
264 17
                    } elseif ($token->keyword === 'CASE') {
265
                        // For a use of CASE like
266
                        // 'SELECT a = CASE .... END, b=1, `id`, ... FROM ...'
267 5
                        $tempCaseExpr = CaseExpression::parse($parser, $list);
268 5
                        $ret->expr .= CaseExpression::build($tempCaseExpr);
269 5
                        $isExpr = true;
270 5
                        continue;
271
                    }
272 12
                    $isExpr = true;
273 67
                } elseif ($brackets === 0 && strlen($ret->expr) > 0 && ! $alias) {
274
                    /* End of expression */
275 48
                    break;
276
                }
277
            }
278
279 345
            if (($token->type === Token::TYPE_NUMBER)
280 337
                || ($token->type === Token::TYPE_BOOL)
281 337
                || (($token->type === Token::TYPE_SYMBOL)
282 337
                && ($token->flags & Token::FLAG_SYMBOL_VARIABLE))
283 337
                || (($token->type === Token::TYPE_SYMBOL)
284 337
                && ($token->flags & Token::FLAG_SYMBOL_PARAMETER))
285 337
                || (($token->type === Token::TYPE_OPERATOR)
286 345
                && ($token->value !== '.'))
287
            ) {
288 222
                if (! empty($options['parseField'])) {
289 70
                    break;
290
                }
291
292
                // Numbers, booleans and operators (except dot) are usually part
293
                // of expressions.
294 178
                $isExpr = true;
295
            }
296
297 345
            if ($token->type === Token::TYPE_OPERATOR) {
298 171
                if (! empty($options['breakOnParentheses'])
299 171
                    && (($token->value === '(') || ($token->value === ')'))
300
                ) {
301
                    // No brackets were expected.
302 1
                    break;
303
                }
304 171
                if ($token->value === '(') {
305 67
                    ++$brackets;
306 67
                    if (empty($ret->function) && ($prev[1] !== null)
307 16
                        && (($prev[1]->type === Token::TYPE_NONE)
308 15
                        || ($prev[1]->type === Token::TYPE_SYMBOL)
309 15
                        || (($prev[1]->type === Token::TYPE_KEYWORD)
310 67
                        && ($prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION)))
311
                    ) {
312 67
                        $ret->function = $prev[1]->value;
313
                    }
314 171
                } elseif ($token->value === ')') {
315 72
                    if ($brackets === 0) {
316
                        // Not our bracket
317 8
                        break;
318
                    } else {
319 67
                        --$brackets;
320 67
                        if ($brackets === 0) {
321 67
                            if (! empty($options['parenthesesDelimited'])) {
322
                                // The current token is the last bracket, the next
323
                                // one will be outside the expression.
324 11
                                $ret->expr .= $token->token;
325 11
                                ++$list->idx;
326 67
                                break;
327
                            }
328 3
                        } elseif ($brackets < 0) {
329
                            // $parser->error('Unexpected closing bracket.', $token);
330
                            // $brackets = 0;
331 59
                            break;
332
                        }
333
                    }
334 157
                } elseif ($token->value === ',') {
335
                    // Expressions are comma-delimited.
336 101
                    if ($brackets === 0) {
337 89
                        break;
338
                    }
339
                }
340
            }
341
342
            // Saving the previous tokens.
343 345
            $prev[0] = $prev[1];
344 345
            $prev[1] = $token;
345
346 345
            if ($alias) {
347
                // An alias is expected (the keyword `AS` was previously found).
348 56
                if (! empty($ret->alias)) {
349 1
                    $parser->error('An alias was previously found.', $token);
350 1
                    break;
351
                }
352 56
                $ret->alias = $token->value;
353 56
                $alias = false;
354 345
            } elseif ($isExpr) {
355
                // Handling aliases.
356 156
                if (/* (empty($ret->alias)) && */ ($brackets === 0)
357 148
                    && (($prev[0] === null)
358 78
                    || ((($prev[0]->type !== Token::TYPE_OPERATOR)
359 78
                    || ($prev[0]->token === ')'))
360 69
                    && (($prev[0]->type !== Token::TYPE_KEYWORD)
361 156
                    || (! ($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED)))))
362 140
                    && (($prev[1]->type === Token::TYPE_STRING)
363 140
                    || (($prev[1]->type === Token::TYPE_SYMBOL)
364 140
                    && (! ($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE)))
365 156
                    || ($prev[1]->type === Token::TYPE_NONE))
366
                ) {
367 6
                    if (! empty($ret->alias)) {
368 2
                        $parser->error('An alias was previously found.', $token);
369 2
                        break;
370
                    }
371 5
                    $ret->alias = $prev[1]->value;
372
                } else {
373 156
                    $ret->expr .= $token->token;
374
                }
375 323
            } elseif (! $isExpr) {
376 323
                if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '.')) {
377
                    // Found a `.` which means we expect a column name and
378
                    // the column name we parsed is actually the table name
379
                    // and the table name is actually a database name.
380 39
                    if (! empty($ret->database) || $dot) {
381 2
                        $parser->error('Unexpected dot.', $token);
382
                    }
383 39
                    $ret->database = $ret->table;
384 39
                    $ret->table = $ret->column;
385 39
                    $ret->column = null;
386 39
                    $dot = true;
387 39
                    $ret->expr .= $token->token;
388
                } else {
389 323
                    $field = empty($options['field']) ? 'column' : $options['field'];
390 323
                    if (empty($ret->$field)) {
391 323
                        $ret->$field = $token->value;
392 323
                        $ret->expr .= $token->token;
393 323
                        $dot = false;
394
                    } else {
395
                        // No alias is expected.
396 23
                        if (! empty($options['breakOnAlias'])) {
397 8
                            break;
398
                        }
399 15
                        if (! empty($ret->alias)) {
400 3
                            $parser->error('An alias was previously found.', $token);
401 3
                            break;
402
                        }
403 13
                        $ret->alias = $token->value;
404
                    }
405
                }
406
            }
407
        }
408
409 348
        if ($alias) {
410 3
            $parser->error(
411 3
                'An alias was expected.',
412 3
                $list->tokens[$list->idx - 1]
413
            );
414
        }
415
416
        // White-spaces might be added at the end.
417 348
        $ret->expr = trim($ret->expr);
418
419 348
        if ($ret->expr === '') {
420 9
            return null;
421
        }
422
423 345
        --$list->idx;
424
425 345
        return $ret;
426
    }
427
428
    /**
429
     * @param Expression|Expression[] $component the component to be built
430
     * @param array                   $options   parameters for building
431
     *
432
     * @return string
433
     */
434 87
    public static function build($component, array $options = array())
435
    {
436 87
        if (is_array($component)) {
437 1
            return implode(', ', $component);
438
        }
439
440 87
        if ($component->expr !== '' && ! is_null($component->expr)) {
441 81
            $ret = $component->expr;
442
        } else {
443 8
            $fields = array();
444 8
            if (isset($component->database) && ($component->database !== '')) {
445 1
                $fields[] = $component->database;
446
            }
447 8
            if (isset($component->table) && ($component->table !== '')) {
448 8
                $fields[] = $component->table;
449
            }
450 8
            if (isset($component->column) && ($component->column !== '')) {
451 1
                $fields[] = $component->column;
452
            }
453 8
            $ret = implode('.', Context::escape($fields));
454
        }
455
456 87
        if (! empty($component->alias)) {
457 11
            $ret .= ' AS ' . Context::escape($component->alias);
458
        }
459
460 87
        return $ret;
461
    }
462
}
463