Passed
Push — master ( 0efc01...ef206b )
by Maurício
02:53
created

Context::loadClosest()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 34
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8

Importance

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