Passed
Branch master (366c16)
by William
03:30
created

Context::isSymbol()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

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