Passed
Pull Request — master (#501)
by
unknown
09:07
created

Context::isComment()   C

Complexity

Conditions 12
Paths 6

Size

Total Lines 35
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 12.0427

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 16
c 1
b 0
f 0
nc 6
nop 2
dl 0
loc 35
ccs 14
cts 15
cp 0.9333
crap 12.0427
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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