Passed
Push — master ( 849bbf...9b56a0 )
by Michal
03:44
created

Context::loadClosest()   C

Complexity

Conditions 8
Paths 10

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8

Importance

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

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
541
     */
542 22
    public static function escape($str, $quote = '`')
543
    {
544 22
        if (is_array($str)) {
545 11
            foreach ($str as $key => $value) {
546 11
                $str[$key] = static::escape($value);
547
            }
548
549 11
            return $str;
550
        }
551
552 22
        if ((static::$MODE & self::SQL_MODE_NO_ENCLOSING_QUOTES)
553 1
            && (!static::isKeyword($str, true))
554
        ) {
555 1
            return $str;
556
        }
557
558 22
        if (static::$MODE & self::SQL_MODE_ANSI_QUOTES) {
559 1
            $quote = '"';
560
        }
561
562 22
        return $quote . str_replace($quote, $quote . $quote, $str) . $quote;
563
    }
564
}
565
566
// Initializing the default context.
567
Context::load();
568