Passed
Push — master ( 6b60b4...e29ed1 )
by William
03:22 queued 12s
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
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 explode;
11
use function intval;
12
use function is_array;
13
use function is_int;
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
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html
151
     * @link https://mariadb.com/kb/en/sql-mode/
152
     *
153
     * @var int
154
     */
155
    public static $MODE = self::SQL_MODE_NONE;
156
157
    public const SQL_MODE_NONE = 0;
158
159
    /**
160
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_allow_invalid_dates
161
     * @link https://mariadb.com/kb/en/sql-mode/#allow_invalid_dates
162
     */
163
    public const SQL_MODE_ALLOW_INVALID_DATES = 1;
164
165
    /**
166
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ansi_quotes
167
     * @link https://mariadb.com/kb/en/sql-mode/#ansi_quotes
168
     */
169
    public const SQL_MODE_ANSI_QUOTES = 2;
170
171
    /** Compatibility mode for Microsoft's SQL server. This is the equivalent of {@see SQL_MODE_ANSI_QUOTES}. */
172
    public const SQL_MODE_COMPAT_MYSQL = 2;
173
174
    /**
175
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_error_for_division_by_zero
176
     * @link https://mariadb.com/kb/en/sql-mode/#error_for_division_by_zero
177
     */
178
    public const SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO = 4;
179
180
    /**
181
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_high_not_precedence
182
     * @link https://mariadb.com/kb/en/sql-mode/#high_not_precedence
183
     */
184
    public const SQL_MODE_HIGH_NOT_PRECEDENCE = 8;
185
186
    /**
187
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_ignore_space
188
     * @link https://mariadb.com/kb/en/sql-mode/#ignore_space
189
     */
190
    public const SQL_MODE_IGNORE_SPACE = 16;
191
192
    /**
193
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_auto_create_user
194
     * @link https://mariadb.com/kb/en/sql-mode/#no_auto_create_user
195
     */
196
    public const SQL_MODE_NO_AUTO_CREATE_USER = 32;
197
198
    /**
199
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_auto_value_on_zero
200
     * @link https://mariadb.com/kb/en/sql-mode/#no_auto_value_on_zero
201
     */
202
    public const SQL_MODE_NO_AUTO_VALUE_ON_ZERO = 64;
203
204
    /**
205
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_backslash_escapes
206
     * @link https://mariadb.com/kb/en/sql-mode/#no_backslash_escapes
207
     */
208
    public const SQL_MODE_NO_BACKSLASH_ESCAPES = 128;
209
210
    /**
211
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_dir_in_create
212
     * @link https://mariadb.com/kb/en/sql-mode/#no_dir_in_create
213
     */
214
    public const SQL_MODE_NO_DIR_IN_CREATE = 256;
215
216
    /**
217
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_engine_substitution
218
     * @link https://mariadb.com/kb/en/sql-mode/#no_engine_substitution
219
     */
220
    public const SQL_MODE_NO_ENGINE_SUBSTITUTION = 512;
221
222
    /**
223
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_field_options
224
     * @link https://mariadb.com/kb/en/sql-mode/#no_field_options
225
     */
226
    public const SQL_MODE_NO_FIELD_OPTIONS = 1024;
227
228
    /**
229
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_key_options
230
     * @link https://mariadb.com/kb/en/sql-mode/#no_key_options
231
     */
232
    public const SQL_MODE_NO_KEY_OPTIONS = 2048;
233
234
    /**
235
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_table_options
236
     * @link https://mariadb.com/kb/en/sql-mode/#no_table_options
237
     */
238
    public const SQL_MODE_NO_TABLE_OPTIONS = 4096;
239
240
    /**
241
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_unsigned_subtraction
242
     * @link https://mariadb.com/kb/en/sql-mode/#no_unsigned_subtraction
243
     */
244
    public const SQL_MODE_NO_UNSIGNED_SUBTRACTION = 8192;
245
246
    /**
247
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_zero_date
248
     * @link https://mariadb.com/kb/en/sql-mode/#no_zero_date
249
     */
250
    public const SQL_MODE_NO_ZERO_DATE = 16384;
251
252
    /**
253
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_no_zero_in_date
254
     * @link https://mariadb.com/kb/en/sql-mode/#no_zero_in_date
255
     */
256
    public const SQL_MODE_NO_ZERO_IN_DATE = 32768;
257
258
    /**
259
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_only_full_group_by
260
     * @link https://mariadb.com/kb/en/sql-mode/#only_full_group_by
261
     */
262
    public const SQL_MODE_ONLY_FULL_GROUP_BY = 65536;
263
264
    /**
265
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_pipes_as_concat
266
     * @link https://mariadb.com/kb/en/sql-mode/#pipes_as_concat
267
     */
268
    public const SQL_MODE_PIPES_AS_CONCAT = 131072;
269
270
    /**
271
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_real_as_float
272
     * @link https://mariadb.com/kb/en/sql-mode/#real_as_float
273
     */
274
    public const SQL_MODE_REAL_AS_FLOAT = 262144;
275
276
    /**
277
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_strict_all_tables
278
     * @link https://mariadb.com/kb/en/sql-mode/#strict_all_tables
279
     */
280
    public const SQL_MODE_STRICT_ALL_TABLES = 524288;
281
282
    /**
283
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_strict_trans_tables
284
     * @link https://mariadb.com/kb/en/sql-mode/#strict_trans_tables
285
     */
286
    public const SQL_MODE_STRICT_TRANS_TABLES = 1048576;
287
288
    /**
289
     * Custom mode.
290
     * The table and column names and any other field that must be escaped will not be.
291
     * Reserved keywords are being escaped regardless this mode is used or not.
292
     */
293
    public const SQL_MODE_NO_ENCLOSING_QUOTES = 1073741824;
294
295
    /**
296
     * Equivalent to {@see SQL_MODE_REAL_AS_FLOAT}, {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES},
297
     * {@see SQL_MODE_IGNORE_SPACE}.
298
     *
299
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_ansi
300
     * @link https://mariadb.com/kb/en/sql-mode/#ansi
301
     */
302
    public const SQL_MODE_ANSI = 393234;
303
304
    /**
305
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
306
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
307
     *
308
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_db2
309
     * @link https://mariadb.com/kb/en/sql-mode/#db2
310
     */
311
    public const SQL_MODE_DB2 = 138258;
312
313
    /**
314
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
315
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS},
316
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
317
     *
318
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_maxdb
319
     * @link https://mariadb.com/kb/en/sql-mode/#maxdb
320
     */
321
    public const SQL_MODE_MAXDB = 138290;
322
323
    /**
324
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
325
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
326
     *
327
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_mssql
328
     * @link https://mariadb.com/kb/en/sql-mode/#mssql
329
     */
330
    public const SQL_MODE_MSSQL = 138258;
331
332
    /**
333
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
334
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS},
335
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
336
     *
337
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_oracle
338
     * @link https://mariadb.com/kb/en/sql-mode/#oracle
339
     */
340
    public const SQL_MODE_ORACLE = 138290;
341
342
    /**
343
     * Equivalent to {@see SQL_MODE_PIPES_AS_CONCAT}, {@see SQL_MODE_ANSI_QUOTES}, {@see SQL_MODE_IGNORE_SPACE},
344
     * {@see SQL_MODE_NO_KEY_OPTIONS}, {@see SQL_MODE_NO_TABLE_OPTIONS}, {@see SQL_MODE_NO_FIELD_OPTIONS}.
345
     *
346
     * @link https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_postgresql
347
     * @link https://mariadb.com/kb/en/sql-mode/#postgresql
348
     */
349
    public const SQL_MODE_POSTGRESQL = 138258;
350
351
    /**
352
     * Equivalent to {@see SQL_MODE_STRICT_TRANS_TABLES}, {@see SQL_MODE_STRICT_ALL_TABLES},
353
     * {@see SQL_MODE_NO_ZERO_IN_DATE}, {@see SQL_MODE_NO_ZERO_DATE}, {@see SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO},
354
     * {@see SQL_MODE_NO_AUTO_CREATE_USER}.
355
     *
356
     * @link https://dev.mysql.com/doc/refman/en/sql-mode.html#sqlmode_traditional
357
     * @link https://mariadb.com/kb/en/sql-mode/#traditional
358
     */
359
    public const SQL_MODE_TRADITIONAL = 1622052;
360
361
    // -------------------------------------------------------------------------
362
    // Keyword.
363
364
    /**
365
     * Checks if the given string is a keyword.
366
     *
367
     * @param string $str        string to be checked
368
     * @param bool   $isReserved checks if the keyword is reserved
369
     *
370
     * @return int|null
371
     */
372 2272
    public static function isKeyword($str, $isReserved = false)
373
    {
374 2272
        $str = strtoupper($str);
375
376 2272
        if (isset(static::$KEYWORDS[$str])) {
377 2216
            if ($isReserved && ! (static::$KEYWORDS[$str] & Token::FLAG_KEYWORD_RESERVED)) {
378 4
                return null;
379
            }
380
381 2216
            return static::$KEYWORDS[$str];
382
        }
383
384 2272
        return null;
385
    }
386
387
    // -------------------------------------------------------------------------
388
    // Operator.
389
390
    /**
391
     * Checks if the given string is an operator.
392
     *
393
     * @param string $str string to be checked
394
     *
395
     * @return int|null the appropriate flag for the operator
396
     */
397 2304
    public static function isOperator($str)
398
    {
399 2304
        if (! isset(static::$OPERATORS[$str])) {
400 2296
            return null;
401
        }
402
403 1628
        return static::$OPERATORS[$str];
404
    }
405
406
    // -------------------------------------------------------------------------
407
    // Whitespace.
408
409
    /**
410
     * Checks if the given character is a whitespace.
411
     *
412
     * @param string $str string to be checked
413
     *
414
     * @return bool
415
     */
416 2324
    public static function isWhitespace($str)
417
    {
418 2324
        return ($str === ' ') || ($str === "\r") || ($str === "\n") || ($str === "\t");
419
    }
420
421
    // -------------------------------------------------------------------------
422
    // Comment.
423
424
    /**
425
     * Checks if the given string is the beginning of a whitespace.
426
     *
427
     * @param string $str string to be checked
428
     * @param mixed  $end
429
     *
430
     * @return int|null the appropriate flag for the comment type
431
     */
432 2304
    public static function isComment($str, $end = false)
433
    {
434 2304
        $len = strlen($str);
435 2304
        if ($len === 0) {
436 4
            return null;
437
        }
438
439
        // If comment is Bash style (#):
440 2304
        if ($str[0] === '#') {
441 16
            return Token::FLAG_COMMENT_BASH;
442
        }
443
444
        // If comment is opening C style (/*), warning, it could be a MySQL command (/*!)
445 2304
        if (($len > 1) && ($str[0] === '/') && ($str[1] === '*')) {
446 136
            return ($len > 2) && ($str[2] === '!') ?
447 136
                Token::FLAG_COMMENT_MYSQL_CMD : Token::FLAG_COMMENT_C;
448
        }
449
450
        // If comment is closing C style (*/), warning, it could conflicts with wildcard and a real opening C style.
451
        // It would looks like the following valid SQL statement: "SELECT */* comment */ FROM...".
452 2304
        if (($len > 1) && ($str[0] === '*') && ($str[1] === '/')) {
453 20
            return Token::FLAG_COMMENT_C;
454
        }
455
456
        // If comment is SQL style (--\s?):
457 2304
        if (($len > 2) && ($str[0] === '-') && ($str[1] === '-') && static::isWhitespace($str[2])) {
458 100
            return Token::FLAG_COMMENT_SQL;
459
        }
460
461 2304
        if (($len === 2) && $end && ($str[0] === '-') && ($str[1] === '-')) {
462 4
            return Token::FLAG_COMMENT_SQL;
463
        }
464
465 2304
        return null;
466
    }
467
468
    // -------------------------------------------------------------------------
469
    // Bool.
470
471
    /**
472
     * Checks if the given string is a boolean value.
473
     * This actually check only for `TRUE` and `FALSE` because `1` or `0` are
474
     * actually numbers and are parsed by specific methods.
475
     *
476
     * @param string $str string to be checked
477
     *
478
     * @return bool
479
     */
480 2272
    public static function isBool($str)
481
    {
482 2272
        $str = strtoupper($str);
483
484 2272
        return ($str === 'TRUE') || ($str === 'FALSE');
485
    }
486
487
    // -------------------------------------------------------------------------
488
    // Number.
489
490
    /**
491
     * Checks if the given character can be a part of a number.
492
     *
493
     * @param string $str string to be checked
494
     *
495
     * @return bool
496
     */
497 4
    public static function isNumber($str)
498
    {
499
        return ($str >= '0') && ($str <= '9') || ($str === '.')
500 4
            || ($str === '-') || ($str === '+') || ($str === 'e') || ($str === 'E');
501
    }
502
503
    // -------------------------------------------------------------------------
504
    // Symbol.
505
506
    /**
507
     * Checks if the given character is the beginning of a symbol. A symbol
508
     * can be either a variable or a field name.
509
     *
510
     * @param string $str string to be checked
511
     *
512
     * @return int|null the appropriate flag for the symbol type
513
     */
514 2272
    public static function isSymbol($str)
515
    {
516 2272
        if (strlen($str) === 0) {
517 4
            return null;
518
        }
519
520 2272
        if ($str[0] === '@') {
521 152
            return Token::FLAG_SYMBOL_VARIABLE;
522
        }
523
524 2268
        if ($str[0] === '`') {
525 596
            return Token::FLAG_SYMBOL_BACKTICK;
526
        }
527
528 2268
        if ($str[0] === ':' || $str[0] === '?') {
529 12
            return Token::FLAG_SYMBOL_PARAMETER;
530
        }
531
532 2268
        return null;
533
    }
534
535
    // -------------------------------------------------------------------------
536
    // String.
537
538
    /**
539
     * Checks if the given character is the beginning of a string.
540
     *
541
     * @param string $str string to be checked
542
     *
543
     * @return int|null the appropriate flag for the string type
544
     */
545 2272
    public static function isString($str)
546
    {
547 2272
        if (strlen($str) === 0) {
548 4
            return null;
549
        }
550
551 2272
        if ($str[0] === '\'') {
552 496
            return Token::FLAG_STRING_SINGLE_QUOTES;
553
        }
554
555 2272
        if ($str[0] === '"') {
556 260
            return Token::FLAG_STRING_DOUBLE_QUOTES;
557
        }
558
559 2272
        return null;
560
    }
561
562
    // -------------------------------------------------------------------------
563
    // Delimiter.
564
565
    /**
566
     * Checks if the given character can be a separator for two lexeme.
567
     *
568
     * @param string $str string to be checked
569
     *
570
     * @return bool
571
     */
572 2272
    public static function isSeparator($str)
573
    {
574
        // NOTES:   Only non alphanumeric ASCII characters may be separators.
575
        //          `~` is the last printable ASCII character.
576
        return ($str <= '~')
577
            && ($str !== '_')
578
            && ($str !== '$')
579
            && (($str < '0') || ($str > '9'))
580
            && (($str < 'a') || ($str > 'z'))
581 2272
            && (($str < 'A') || ($str > 'Z'));
582
    }
583
584
    /**
585
     * Loads the specified context.
586
     *
587
     * Contexts may be used by accessing the context directly.
588
     *
589
     * @param string $context name of the context or full class name that defines the context
590
     *
591
     * @return void
592
     *
593
     * @throws LoaderException if the specified context doesn't exist.
594
     */
595 2964
    public static function load($context = '')
596
    {
597 2964
        if (empty($context)) {
598 2964
            $context = self::$defaultContext;
599
        }
600
601 2964
        if ($context[0] !== '\\') {
602
            // Short context name (must be formatted into class name).
603 84
            $context = self::$contextPrefix . $context;
604
        }
605
606 2964
        if (! class_exists($context)) {
607 24
            throw @new LoaderException('Specified context ("' . $context . '") does not exist.', $context);
608
        }
609
610 2964
        self::$loadedContext = $context;
611 2964
        self::$KEYWORDS = $context::$KEYWORDS;
0 ignored issues
show
Bug introduced by
The property KEYWORDS does not exist on string.
Loading history...
612
    }
613
614
    /**
615
     * Loads the context with the closest version to the one specified.
616
     *
617
     * The closest context is found by replacing last digits with zero until one
618
     * is loaded successfully.
619
     *
620
     * @see Context::load()
621
     *
622
     * @param string $context name of the context or full class name that
623
     *                        defines the context
624
     *
625
     * @return string|null The loaded context. `null` if no context was loaded.
626
     */
627 28
    public static function loadClosest($context = '')
628
    {
629 28
        $length = strlen($context);
630 28
        for ($i = $length; $i > 0;) {
631
            try {
632
                /* Trying to load the new context */
633 28
                static::load($context);
634
635 24
                return $context;
636 20
            } catch (LoaderException $e) {
637
                /* Replace last two non zero digits by zeroes */
638
                do {
639 20
                    $i -= 2;
640 20
                    $part = substr($context, $i, 2);
641
                    /* No more numeric parts to strip */
642 20
                    if (! is_numeric($part)) {
643 12
                        break 2;
644
                    }
645 16
                } while (intval($part) === 0 && $i > 0);
646
647 16
                $context = substr($context, 0, $i) . '00' . substr($context, $i + 2);
648
            }
649
        }
650
651
        /* Fallback to loading at least matching engine */
652 12
        if (str_starts_with($context, 'MariaDb')) {
653 4
            return static::loadClosest('MariaDb100300');
654
        }
655
656 8
        if (str_starts_with($context, 'MySql')) {
657 4
            return static::loadClosest('MySql50700');
658
        }
659
660 4
        return null;
661
    }
662
663
    /**
664
     * Gets the SQL mode.
665
     */
666 144
    public static function getMode(): int
667
    {
668 144
        return static::$MODE;
669
    }
670
671
    /**
672
     * Sets the SQL mode.
673
     *
674
     * @param int|string $mode
675
     *
676
     * @return void
677
     */
678 1452
    public static function setMode($mode = self::SQL_MODE_NONE)
679
    {
680 1452
        if (is_int($mode)) {
681 1320
            static::$MODE = $mode;
682
683 1320
            return;
684
        }
685
686 132
        static::$MODE = self::SQL_MODE_NONE;
687 132
        if ($mode === '') {
688 8
            return;
689
        }
690
691 128
        $modes = explode(',', $mode);
692 128
        foreach ($modes as $sqlMode) {
693 128
            static::$MODE |= self::getModeFromString($sqlMode);
694
        }
695
    }
696
697
    /**
698
     * @psalm-suppress MixedReturnStatement, MixedInferredReturnType Is caused by the LSB of the constants
699
     */
700 128
    private static function getModeFromString(string $mode): int
701
    {
702
        // phpcs:disable SlevomatCodingStandard.Classes.DisallowLateStaticBindingForConstants.DisallowedLateStaticBindingForConstant
703
        switch ($mode) {
704 128
            case 'ALLOW_INVALID_DATES':
705 4
                return static::SQL_MODE_ALLOW_INVALID_DATES;
706
707 124
            case 'ANSI_QUOTES':
708 8
                return static::SQL_MODE_ANSI_QUOTES;
709
710 120
            case 'COMPAT_MYSQL':
711 4
                return static::SQL_MODE_COMPAT_MYSQL;
712
713 116
            case 'ERROR_FOR_DIVISION_BY_ZERO':
714 4
                return static::SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO;
715
716 112
            case 'HIGH_NOT_PRECEDENCE':
717 4
                return static::SQL_MODE_HIGH_NOT_PRECEDENCE;
718
719 108
            case 'IGNORE_SPACE':
720 8
                return static::SQL_MODE_IGNORE_SPACE;
721
722 104
            case 'NO_AUTO_CREATE_USER':
723 4
                return static::SQL_MODE_NO_AUTO_CREATE_USER;
724
725 100
            case 'NO_AUTO_VALUE_ON_ZERO':
726 4
                return static::SQL_MODE_NO_AUTO_VALUE_ON_ZERO;
727
728 96
            case 'NO_BACKSLASH_ESCAPES':
729 4
                return static::SQL_MODE_NO_BACKSLASH_ESCAPES;
730
731 92
            case 'NO_DIR_IN_CREATE':
732 4
                return static::SQL_MODE_NO_DIR_IN_CREATE;
733
734 88
            case 'NO_ENGINE_SUBSTITUTION':
735 4
                return static::SQL_MODE_NO_ENGINE_SUBSTITUTION;
736
737 84
            case 'NO_FIELD_OPTIONS':
738 4
                return static::SQL_MODE_NO_FIELD_OPTIONS;
739
740 80
            case 'NO_KEY_OPTIONS':
741 4
                return static::SQL_MODE_NO_KEY_OPTIONS;
742
743 76
            case 'NO_TABLE_OPTIONS':
744 4
                return static::SQL_MODE_NO_TABLE_OPTIONS;
745
746 72
            case 'NO_UNSIGNED_SUBTRACTION':
747 4
                return static::SQL_MODE_NO_UNSIGNED_SUBTRACTION;
748
749 68
            case 'NO_ZERO_DATE':
750 4
                return static::SQL_MODE_NO_ZERO_DATE;
751
752 64
            case 'NO_ZERO_IN_DATE':
753 4
                return static::SQL_MODE_NO_ZERO_IN_DATE;
754
755 60
            case 'ONLY_FULL_GROUP_BY':
756 4
                return static::SQL_MODE_ONLY_FULL_GROUP_BY;
757
758 56
            case 'PIPES_AS_CONCAT':
759 4
                return static::SQL_MODE_PIPES_AS_CONCAT;
760
761 52
            case 'REAL_AS_FLOAT':
762 8
                return static::SQL_MODE_REAL_AS_FLOAT;
763
764 48
            case 'STRICT_ALL_TABLES':
765 4
                return static::SQL_MODE_STRICT_ALL_TABLES;
766
767 44
            case 'STRICT_TRANS_TABLES':
768 4
                return static::SQL_MODE_STRICT_TRANS_TABLES;
769
770 40
            case 'NO_ENCLOSING_QUOTES':
771 4
                return static::SQL_MODE_NO_ENCLOSING_QUOTES;
772
773 36
            case 'ANSI':
774 4
                return static::SQL_MODE_ANSI;
775
776 32
            case 'DB2':
777 4
                return static::SQL_MODE_DB2;
778
779 28
            case 'MAXDB':
780 4
                return static::SQL_MODE_MAXDB;
781
782 24
            case 'MSSQL':
783 4
                return static::SQL_MODE_MSSQL;
784
785 20
            case 'ORACLE':
786 4
                return static::SQL_MODE_ORACLE;
787
788 16
            case 'POSTGRESQL':
789 4
                return static::SQL_MODE_POSTGRESQL;
790
791 12
            case 'TRADITIONAL':
792 8
                return static::SQL_MODE_TRADITIONAL;
793
794
            default:
795 4
                return self::SQL_MODE_NONE;
796
        }
797
        // phpcs:enable
798
    }
799
800
    /**
801
     * Escapes the symbol by adding surrounding backticks.
802
     *
803
     * @param string[]|string $str   the string to be escaped
804
     * @param string          $quote quote to be used when escaping
805
     *
806
     * @return string|string[]
807
     */
808 256
    public static function escape($str, $quote = '`')
809
    {
810 256
        if (is_array($str)) {
811 60
            foreach ($str as $key => $value) {
812 60
                $str[$key] = static::escape($value);
813
            }
814
815 60
            return $str;
816
        }
817
818 256
        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...
819 4
            return $str;
820
        }
821
822 256
        if (static::$MODE & self::SQL_MODE_ANSI_QUOTES) {
823 4
            $quote = '"';
824
        }
825
826 256
        return $quote . str_replace($quote, $quote . $quote, $str) . $quote;
827
    }
828
829
    /**
830
     * Returns char used to quote identifiers based on currently set SQL Mode (ie. standard or ANSI_QUOTES)
831
     *
832
     * @return string either " (double quote, ansi_quotes mode) or ` (backtick, standard mode)
833
     */
834
    public static function getIdentifierQuote()
835
    {
836
        return self::hasMode(self::SQL_MODE_ANSI_QUOTES) ? '"' : '`';
837
    }
838
839
    /**
840
     * Function verifies that given SQL Mode constant is currently set
841
     *
842
     * @param int $flag for example {@see Context::SQL_MODE_ANSI_QUOTES}
843
     *
844
     * @return bool false on empty param, true/false on given constant/int value
845
     */
846 8
    public static function hasMode($flag = null)
847
    {
848 8
        if (empty($flag)) {
849
            return false;
850
        }
851
852 8
        return (self::$MODE & $flag) === $flag;
853
    }
854
}
855
856
// Initializing the default context.
857
Context::load();
858