Passed
Pull Request — master (#369)
by Maurício
21:47 queued 11:46
created

Context::getMode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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