Passed
Pull Request — master (#294)
by William
04:00
created

Context::isBool()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
/**
4
 * Defines a context class that is later extended to define other contexts.
5
 *
6
 * A context is a collection of keywords, operators and functions used for
7
 * parsing.
8
 */
9
10
namespace PhpMyAdmin\SqlParser;
11
12
use PhpMyAdmin\SqlParser\Exceptions\LoaderException;
13
14
/**
15
 * Holds the configuration of the context that is currently used.
16
 *
17
 * @category Contexts
18
 *
19
 * @license  https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
20
 */
21
abstract class Context
22
{
23
    /**
24
     * The maximum length of a keyword.
25
     *
26
     * @see static::$TOKEN_KEYWORD
27
     *
28
     * @var int
29
     */
30
    const KEYWORD_MAX_LENGTH = 30;
31
32
    /**
33
     * The maximum length of a label.
34
     *
35
     * @see static::$TOKEN_LABEL
36
     * Ref: https://dev.mysql.com/doc/refman/5.7/en/statement-labels.html
37
     *
38
     * @var int
39
     */
40
    const LABEL_MAX_LENGTH = 16;
41
42
    /**
43
     * The maximum length of an operator.
44
     *
45
     * @see static::$TOKEN_OPERATOR
46
     *
47
     * @var int
48
     */
49
    const OPERATOR_MAX_LENGTH = 4;
50
51
    /**
52
     * The name of the default content.
53
     *
54
     * @var string
55
     */
56
    public static $defaultContext = '\\PhpMyAdmin\\SqlParser\\Contexts\\ContextMySql50700';
57
58
    /**
59
     * The name of the loaded context.
60
     *
61
     * @var string
62
     */
63
    public static $loadedContext = '\\PhpMyAdmin\\SqlParser\\Contexts\\ContextMySql50700';
64
65
    /**
66
     * The prefix concatenated to the context name when an incomplete class name
67
     * is specified.
68
     *
69
     * @var string
70
     */
71
    public static $contextPrefix = '\\PhpMyAdmin\\SqlParser\\Contexts\\Context';
72
73
    /**
74
     * List of keywords.
75
     *
76
     * Because, PHP's associative arrays are basically hash tables, it is more
77
     * efficient to store keywords as keys instead of values.
78
     *
79
     * The value associated to each keyword represents its flags.
80
     *
81
     * @see Token::FLAG_KEYWORD_RESERVED Token::FLAG_KEYWORD_COMPOSED
82
     *      Token::FLAG_KEYWORD_DATA_TYPE Token::FLAG_KEYWORD_KEY
83
     *      Token::FLAG_KEYWORD_FUNCTION
84
     *
85
     * Elements are sorted by flags, length and keyword.
86
     *
87
     * @var array
88
     */
89
    public static $KEYWORDS = array();
90
91
    /**
92
     * List of operators and their flags.
93
     *
94
     * @var array
95
     */
96
    public static $OPERATORS = array(
97
        // Some operators (*, =) may have ambiguous flags, because they depend on
98
        // the context they are being used in.
99
        // For example: 1. SELECT * FROM table; # SQL specific (wildcard)
100
        //                 SELECT 2 * 3;        # arithmetic
101
        //              2. SELECT * FROM table WHERE foo = 'bar';
102
        //                 SET @i = 0;
103
104
        // @see Token::FLAG_OPERATOR_ARITHMETIC
105
        '%' => 1,
106
        '*' => 1,
107
        '+' => 1,
108
        '-' => 1,
109
        '/' => 1,
110
111
        // @see Token::FLAG_OPERATOR_LOGICAL
112
        '!' => 2,
113
        '!=' => 2,
114
        '&&' => 2,
115
        '<' => 2,
116
        '<=' => 2,
117
        '<=>' => 2,
118
        '<>' => 2,
119
        '=' => 2,
120
        '>' => 2,
121
        '>=' => 2,
122
        '||' => 2,
123
124
        // @see Token::FLAG_OPERATOR_BITWISE
125
        '&' => 4,
126
        '<<' => 4,
127
        '>>' => 4,
128
        '^' => 4,
129
        '|' => 4,
130
        '~' => 4,
131
132
        // @see Token::FLAG_OPERATOR_ASSIGNMENT
133
        ':=' => 8,
134
135
        // @see Token::FLAG_OPERATOR_SQL
136
        '(' => 16,
137
        ')' => 16,
138
        '.' => 16,
139
        ',' => 16,
140
        ';' => 16
141
    );
142
143
    /**
144
     * The mode of the MySQL server that will be used in lexing, parsing and
145
     * building the statements.
146
     *
147
     * @var int
148
     */
149
    public static $MODE = 0;
150
151
    /*
152
     * Server SQL Modes
153
     * https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html
154
     */
155
156
    // Compatibility mode for Microsoft's SQL server.
157
    // This is the equivalent of ANSI_QUOTES.
158
    const SQL_MODE_COMPAT_MYSQL = 2;
159
160
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_allow_invalid_dates
161
    const SQL_MODE_ALLOW_INVALID_DATES = 1;
162
163
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_ansi_quotes
164
    const SQL_MODE_ANSI_QUOTES = 2;
165
166
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_error_for_division_by_zero
167
    const SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO = 4;
168
169
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_high_not_precedence
170
    const SQL_MODE_HIGH_NOT_PRECEDENCE = 8;
171
172
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_ignore_space
173
    const SQL_MODE_IGNORE_SPACE = 16;
174
175
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_auto_create_user
176
    const SQL_MODE_NO_AUTO_CREATE_USER = 32;
177
178
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_auto_value_on_zero
179
    const SQL_MODE_NO_AUTO_VALUE_ON_ZERO = 64;
180
181
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_backslash_escapes
182
    const SQL_MODE_NO_BACKSLASH_ESCAPES = 128;
183
184
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_dir_in_create
185
    const SQL_MODE_NO_DIR_IN_CREATE = 256;
186
187
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_dir_in_create
188
    const SQL_MODE_NO_ENGINE_SUBSTITUTION = 512;
189
190
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_field_options
191
    const SQL_MODE_NO_FIELD_OPTIONS = 1024;
192
193
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_key_options
194
    const SQL_MODE_NO_KEY_OPTIONS = 2048;
195
196
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_table_options
197
    const SQL_MODE_NO_TABLE_OPTIONS = 4096;
198
199
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_unsigned_subtraction
200
    const SQL_MODE_NO_UNSIGNED_SUBTRACTION = 8192;
201
202
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_zero_date
203
    const SQL_MODE_NO_ZERO_DATE = 16384;
204
205
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_zero_in_date
206
    const SQL_MODE_NO_ZERO_IN_DATE = 32768;
207
208
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_only_full_group_by
209
    const SQL_MODE_ONLY_FULL_GROUP_BY = 65536;
210
211
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_pipes_as_concat
212
    const SQL_MODE_PIPES_AS_CONCAT = 131072;
213
214
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_real_as_float
215
    const SQL_MODE_REAL_AS_FLOAT = 262144;
216
217
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_strict_all_tables
218
    const SQL_MODE_STRICT_ALL_TABLES = 524288;
219
220
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_strict_trans_tables
221
    const SQL_MODE_STRICT_TRANS_TABLES = 1048576;
222
223
    // Custom modes.
224
225
    // The table and column names and any other field that must be escaped will
226
    // not be.
227
    // Reserved keywords are being escaped regardless this mode is used or not.
228
    const SQL_MODE_NO_ENCLOSING_QUOTES = 1073741824;
229
230
    /*
231
     * Combination SQL Modes
232
     * https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sql-mode-combo
233
     */
234
235
    // REAL_AS_FLOAT, PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE
236
    const SQL_MODE_ANSI = 393234;
237
238
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
239
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS,
240
    const SQL_MODE_DB2 = 138258;
241
242
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
243
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS, NO_AUTO_CREATE_USER
244
    const SQL_MODE_MAXDB = 138290;
245
246
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
247
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS
248
    const SQL_MODE_MSSQL = 138258;
249
250
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
251
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS, NO_AUTO_CREATE_USER
252
    const SQL_MODE_ORACLE = 138290;
253
254
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
255
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS
256
    const SQL_MODE_POSTGRESQL = 138258;
257
258
    // STRICT_TRANS_TABLES, STRICT_ALL_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE,
259
    // ERROR_FOR_DIVISION_BY_ZERO, NO_AUTO_CREATE_USER
260
    const SQL_MODE_TRADITIONAL = 1622052;
261
262
    // -------------------------------------------------------------------------
263
    // Keyword.
264
265
    /**
266
     * Checks if the given string is a keyword.
267
     *
268
     * @param string $str        string to be checked
269
     * @param bool   $isReserved checks if the keyword is reserved
270
     *
271
     * @return int|null
272
     */
273 465
    public static function isKeyword($str, $isReserved = false)
274
    {
275 465
        $str = strtoupper($str);
276
277 465
        if (isset(static::$KEYWORDS[$str])) {
278 449
            if ($isReserved && ! (static::$KEYWORDS[$str] & Token::FLAG_KEYWORD_RESERVED)) {
279 1
                return null;
280
            }
281
282 449
            return static::$KEYWORDS[$str];
283
        }
284
285 463
        return null;
286
    }
287
288
    // -------------------------------------------------------------------------
289
    // Operator.
290
291
    /**
292
     * Checks if the given string is an operator.
293
     *
294
     * @param string $str string to be checked
295
     *
296
     * @return int|null the appropriate flag for the operator
297
     */
298 473
    public static function isOperator($str)
299
    {
300 473
        if (! isset(static::$OPERATORS[$str])) {
301 471
            return null;
302
        }
303
304 328
        return static::$OPERATORS[$str];
305
    }
306
307
    // -------------------------------------------------------------------------
308
    // Whitespace.
309
310
    /**
311
     * Checks if the given character is a whitespace.
312
     *
313
     * @param string $str string to be checked
314
     *
315
     * @return bool
316
     */
317 478
    public static function isWhitespace($str)
318
    {
319 478
        return ($str === ' ') || ($str === "\r") || ($str === "\n") || ($str === "\t");
320
    }
321
322
    // -------------------------------------------------------------------------
323
    // Comment.
324
325
    /**
326
     * Checks if the given string is the beginning of a whitespace.
327
     *
328
     * @param string $str string to be checked
329
     * @param mixed  $end
330
     *
331
     * @return int|null the appropriate flag for the comment type
332
     */
333 473
    public static function isComment($str, $end = false)
334
    {
335 473
        $len = strlen($str);
336 473
        if ($len === 0) {
337 1
            return null;
338
        }
339
340
        // If comment is Bash style (#):
341 473
        if ($str[0] === '#') {
342 4
            return Token::FLAG_COMMENT_BASH;
343
        }
344
        // If comment is opening C style (/*), warning, it could be a MySQL command (/*!)
345 473
        if (($len > 1) && ($str[0] === '/') && ($str[1] === '*')) {
346 30
            return ($len > 2) && ($str[2] === '!') ?
347 30
                Token::FLAG_COMMENT_MYSQL_CMD : Token::FLAG_COMMENT_C;
348
        }
349
        // If comment is closing C style (*/), warning, it could conflicts with wildcard and a real opening C style.
350
        // It would looks like the following valid SQL statement: "SELECT */* comment */ FROM...".
351 473
        if (($len > 1) && ($str[0] === '*') && ($str[1] === '/')) {
352 4
            return Token::FLAG_COMMENT_C;
353
        }
354
        // If comment is SQL style (--\s?):
355 473 View Code Duplication
        if (($len > 2) && ($str[0] === '-')
356 17
            && ($str[1] === '-') && static::isWhitespace($str[2])
357
        ) {
358 17
            return Token::FLAG_COMMENT_SQL;
359
        }
360 473 View Code Duplication
        if (($len === 2) && $end && ($str[0] === '-') && ($str[1] === '-')) {
361 1
            return Token::FLAG_COMMENT_SQL;
362
        }
363
364 473
        return null;
365
    }
366
367
    // -------------------------------------------------------------------------
368
    // Bool.
369
370
    /**
371
     * Checks if the given string is a boolean value.
372
     * This actually check only for `TRUE` and `FALSE` because `1` or `0` are
373
     * actually numbers and are parsed by specific methods.
374
     *
375
     * @param string $str string to be checked
376
     *
377
     * @return bool
378
     */
379 465
    public static function isBool($str)
380
    {
381 465
        $str = strtoupper($str);
382
383 465
        return ($str === 'TRUE') || ($str === 'FALSE');
384
    }
385
386
    // -------------------------------------------------------------------------
387
    // Number.
388
389
    /**
390
     * Checks if the given character can be a part of a number.
391
     *
392
     * @param string $str string to be checked
393
     *
394
     * @return bool
395
     */
396 1
    public static function isNumber($str)
397
    {
398 1
        return (($str >= '0') && ($str <= '9')) || ($str === '.')
399 1
            || ($str === '-') || ($str === '+') || ($str === 'e') || ($str === 'E');
400
    }
401
402
    // -------------------------------------------------------------------------
403
    // Symbol.
404
405
    /**
406
     * Checks if the given character is the beginning of a symbol. A symbol
407
     * can be either a variable or a field name.
408
     *
409
     * @param string $str string to be checked
410
     *
411
     * @return int|null the appropriate flag for the symbol type
412
     */
413 465 View Code Duplication
    public static function isSymbol($str)
414
    {
415 465
        if (strlen($str) === 0) {
416 1
            return null;
417
        }
418 465
        if ($str[0] === '@') {
419 32
            return Token::FLAG_SYMBOL_VARIABLE;
420 464
        } elseif ($str[0] === self::getIdentifierQuote()) {
421 108
            return Token::FLAG_SYMBOL_BACKTICK;
422 464
        } elseif ($str[0] === ':' || $str[0] === '?') {
423 3
            return Token::FLAG_SYMBOL_PARAMETER;
424
        }
425
426 464
        return null;
427
    }
428
429
    // -------------------------------------------------------------------------
430
    // String.
431
432
    /**
433
     * Checks if the given character is the beginning of a string.
434
     *
435
     * @param string $str string to be checked
436
     *
437
     * @return int|null the appropriate flag for the string type
438
     */
439 465 View Code Duplication
    public static function isString($str)
440
    {
441 465
        if (strlen($str) === 0) {
442 1
            return null;
443
        }
444 465
        if ($str[0] === '\'') {
445 90
            return Token::FLAG_STRING_SINGLE_QUOTES;
446 465
        } elseif (self::hasMode(self::SQL_MODE_ANSI_QUOTES) && $str[0] === '"') {
447 1
            return null;
448 465
        } elseif ($str[0] === '"') {
449 63
            return Token::FLAG_STRING_DOUBLE_QUOTES;
450
        }
451
452 465
        return null;
453
    }
454
455
    // -------------------------------------------------------------------------
456
    // Delimiter.
457
458
    /**
459
     * Checks if the given character can be a separator for two lexeme.
460
     *
461
     * @param string $str string to be checked
462
     *
463
     * @return bool
464
     */
465 465
    public static function isSeparator($str)
466
    {
467
        // NOTES:   Only non alphanumeric ASCII characters may be separators.
468
        //          `~` is the last printable ASCII character.
469 465
        return ($str <= '~') && ($str !== '_')
470 465
            && (($str < '0') || ($str > '9'))
471 465
            && (($str < 'a') || ($str > 'z'))
472 465
            && (($str < 'A') || ($str > 'Z'));
473
    }
474
475
    /**
476
     * Loads the specified context.
477
     *
478
     * Contexts may be used by accessing the context directly.
479
     *
480
     * @param string $context name of the context or full class name that
481
     *                        defines the context
482
     *
483
     * @throws LoaderException if the specified context doesn't exist
484
     */
485 21
    public static function load($context = '')
486
    {
487 21
        if (empty($context)) {
488 18
            $context = self::$defaultContext;
489
        }
490 21
        if ($context[0] !== '\\') {
491
            // Short context name (must be formatted into class name).
492 20
            $context = self::$contextPrefix . $context;
493
        }
494 21
        if (! class_exists($context)) {
495 6
            throw @new LoaderException(
496 6
                'Specified context ("' . $context . '") does not exist.',
497 6
                $context
498
            );
499
        }
500 20
        self::$loadedContext = $context;
501 20
        self::$KEYWORDS = $context::$KEYWORDS;
502 20
    }
503
504
    /**
505
     * Loads the context with the closest version to the one specified.
506
     *
507
     * The closest context is found by replacing last digits with zero until one
508
     * is loaded successfully.
509
     *
510
     * @see Context::load()
511
     *
512
     * @param string $context name of the context or full class name that
513
     *                        defines the context
514
     *
515
     * @return string|null The loaded context. `null` if no context was loaded.
516
     */
517 7
    public static function loadClosest($context = '')
518
    {
519 7
        $length = strlen($context);
520 7
        for ($i = $length; $i > 0;) {
521
            try {
522
                /* Trying to load the new context */
523 7
                static::load($context);
524 6
                return $context;
525 5
            } catch (LoaderException $e) {
526
                /* Replace last two non zero digits by zeroes */
527
                do {
528 5
                    $i -= 2;
529 5
                    $part = substr($context, $i, 2);
530
                    /* No more numeric parts to strip */
531 5
                    if (! is_numeric($part)) {
532 3
                        break 2;
533
                    }
534 4
                } while (intval($part) === 0 && $i > 0);
535 4
                $context = substr($context, 0, $i) . '00' . substr($context, $i + 2);
536
            }
537
        }
538
        /* Fallback to loading at least matching engine */
539 3
        if (strncmp($context, 'MariaDb', 7) === 0) {
540 1
            return static::loadClosest('MariaDb100300');
541 2
        } elseif (strncmp($context, 'MySql', 5) === 0) {
542 1
            return static::loadClosest('MySql50700');
543
        }
544 1
        return null;
545
    }
546
547
    /**
548
     * Sets the SQL mode.
549
     *
550
     * @param string $mode The list of modes. If empty, the mode is reset.
551
     */
552 259
    public static function setMode($mode = '')
553
    {
554 259
        static::$MODE = 0;
555 259
        if (empty($mode)) {
556 259
            return;
557
        }
558 3
        $mode = explode(',', $mode);
559 3
        foreach ($mode as $m) {
560 3
            static::$MODE |= constant('static::SQL_MODE_' . $m);
561
        }
562 3
    }
563
564
    /**
565
     * Escapes the symbol by adding surrounding backticks.
566
     *
567
     * @param array|string $str   the string to be escaped
568
     * @param string       $quote quote to be used when escaping
569
     *
570
     * @return string|array
571
     */
572 39
    public static function escape($str, $quote = '`')
573
    {
574 39
        if (is_array($str)) {
575 12
            foreach ($str as $key => $value) {
576 12
                $str[$key] = static::escape($value);
577
            }
578
579 12
            return $str;
580
        }
581
582 39
        if ((static::$MODE & self::SQL_MODE_NO_ENCLOSING_QUOTES)
583 1
            && (! static::isKeyword($str, true))
0 ignored issues
show
Bug Best Practice introduced by
The expression static::isKeyword($str, true) of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
584
        ) {
585 1
            return $str;
586
        }
587
588 39
        if (static::$MODE & self::SQL_MODE_ANSI_QUOTES) {
589 1
            $quote = '"';
590
        }
591
592 39
        return $quote . str_replace($quote, $quote . $quote, $str) . $quote;
593
    }
594
595
    /**
596
     * Returns char used to quote identifiers based on currently set SQL Mode (ie. standard or ANSI_QUOTES)
597
     * @return string either " (double quote, ansi_quotes mode) or ` (backtick, standard mode)
598
     */
599 465
    public static function getIdentifierQuote()
600
    {
601 465
        return self::hasMode(self::SQL_MODE_ANSI_QUOTES) ? '"' : '`';
602
    }
603
604
    /**
605
     * Function verifies that given SQL Mode constant is currently set
606
     *
607
     * @return boolean false on empty param, true/false on given constant/int value
608
     * @param int $flag for example Context::SQL_MODE_ANSI_QUOTES
0 ignored issues
show
Documentation introduced by
Should the type for parameter $flag not be integer|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...
609
     */
610 466
    public static function hasMode($flag = null)
611
    {
612 466
        if (empty($flag)) {
613
            return false;
614
        }
615 466
        return (self::$MODE & $flag) === $flag;
616
    }
617
}
618
619
// Initializing the default context.
620
Context::load();
621