Completed
Push — master ( 14c8dc...87c59f )
by Michal
23:27
created

Context::isSymbol()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 13
Ratio 100 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 13
loc 13
ccs 7
cts 8
cp 0.875
rs 9.2
cc 4
eloc 8
nc 4
nop 1
crap 4.0312
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_*
82
     *
83
     * Elements are sorted by flags, length and keyword.
84
     *
85
     * @var array
86
     */
87
    public static $KEYWORDS = array();
88
89
    /**
90
     * List of operators and their flags.
91
     *
92
     * @var array
93
     */
94
    public static $OPERATORS = array(
95
        // Some operators (*, =) may have ambiguous flags, because they depend on
96
        // the context they are being used in.
97
        // For example: 1. SELECT * FROM table; # SQL specific (wildcard)
98
        //                 SELECT 2 * 3;        # arithmetic
99
        //              2. SELECT * FROM table WHERE foo = 'bar';
100
        //                 SET @i = 0;
101
102
        // @see Token::FLAG_OPERATOR_ARITHMETIC
103
        '%' => 1, '*' => 1, '+' => 1, '-' => 1, '/' => 1,
104
105
        // @see Token::FLAG_OPERATOR_LOGICAL
106
        '!' => 2, '!=' => 2, '&&' => 2, '<' => 2, '<=' => 2,
107
        '<=>' => 2, '<>' => 2, '=' => 2, '>' => 2, '>=' => 2,
108
        '||' => 2,
109
110
        // @see Token::FLAG_OPERATOR_BITWISE
111
        '&' => 4, '<<' => 4, '>>' => 4, '^' => 4, '|' => 4,
112
        '~' => 4,
113
114
        // @see Token::FLAG_OPERATOR_ASSIGNMENT
115
        ':=' => 8,
116
117
        // @see Token::FLAG_OPERATOR_SQL
118
        '(' => 16, ')' => 16, '.' => 16, ',' => 16, ';' => 16,
119
    );
120
121
    /**
122
     * The mode of the MySQL server that will be used in lexing, parsing and
123
     * building the statements.
124
     *
125
     * @var int
126
     */
127
    public static $MODE = 0;
128
129
    /*
130
     * Server SQL Modes
131
     * https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html
132
     */
133
134
    // Compatibility mode for Microsoft's SQL server.
135
    // This is the equivalent of ANSI_QUOTES.
136
    const SQL_MODE_COMPAT_MYSQL = 2;
137
138
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_allow_invalid_dates
139
    const SQL_MODE_ALLOW_INVALID_DATES = 1;
140
141
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_ansi_quotes
142
    const SQL_MODE_ANSI_QUOTES = 2;
143
144
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_error_for_division_by_zero
145
    const SQL_MODE_ERROR_FOR_DIVISION_BY_ZERO = 4;
146
147
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_high_not_precedence
148
    const SQL_MODE_HIGH_NOT_PRECEDENCE = 8;
149
150
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_ignore_space
151
    const SQL_MODE_IGNORE_SPACE = 16;
152
153
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_auto_create_user
154
    const SQL_MODE_NO_AUTO_CREATE_USER = 32;
155
156
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_auto_value_on_zero
157
    const SQL_MODE_NO_AUTO_VALUE_ON_ZERO = 64;
158
159
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_backslash_escapes
160
    const SQL_MODE_NO_BACKSLASH_ESCAPES = 128;
161
162
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_dir_in_create
163
    const SQL_MODE_NO_DIR_IN_CREATE = 256;
164
165
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_dir_in_create
166
    const SQL_MODE_NO_ENGINE_SUBSTITUTION = 512;
167
168
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_field_options
169
    const SQL_MODE_NO_FIELD_OPTIONS = 1024;
170
171
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_key_options
172
    const SQL_MODE_NO_KEY_OPTIONS = 2048;
173
174
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_table_options
175
    const SQL_MODE_NO_TABLE_OPTIONS = 4096;
176
177
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_unsigned_subtraction
178
    const SQL_MODE_NO_UNSIGNED_SUBTRACTION = 8192;
179
180
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_zero_date
181
    const SQL_MODE_NO_ZERO_DATE = 16384;
182
183
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_no_zero_in_date
184
    const SQL_MODE_NO_ZERO_IN_DATE = 32768;
185
186
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_only_full_group_by
187
    const SQL_MODE_ONLY_FULL_GROUP_BY = 65536;
188
189
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_pipes_as_concat
190
    const SQL_MODE_PIPES_AS_CONCAT = 131072;
191
192
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_real_as_float
193
    const SQL_MODE_REAL_AS_FLOAT = 262144;
194
195
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_strict_all_tables
196
    const SQL_MODE_STRICT_ALL_TABLES = 524288;
197
198
    // https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sqlmode_strict_trans_tables
199
    const SQL_MODE_STRICT_TRANS_TABLES = 1048576;
200
201
    // Custom modes.
202
203
    // The table and column names and any other field that must be escaped will
204
    // not be.
205
    // Reserved keywords are being escaped regardless this mode is used or not.
206
    const SQL_MODE_NO_ENCLOSING_QUOTES = 1073741824;
207
208
    /*
209
     * Combination SQL Modes
210
     * https://dev.mysql.com/doc/refman/5.0/en/sql-mode.html#sql-mode-combo
211
     */
212
213
    // REAL_AS_FLOAT, PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE
214
    const SQL_MODE_ANSI = 393234;
215
216
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
217
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS,
218
    const SQL_MODE_DB2 = 138258;
219
220
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
221
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS, NO_AUTO_CREATE_USER
222
    const SQL_MODE_MAXDB = 138290;
223
224
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
225
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS
226
    const SQL_MODE_MSSQL = 138258;
227
228
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
229
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS, NO_AUTO_CREATE_USER
230
    const SQL_MODE_ORACLE = 138290;
231
232
    // PIPES_AS_CONCAT, ANSI_QUOTES, IGNORE_SPACE, NO_KEY_OPTIONS,
233
    // NO_TABLE_OPTIONS, NO_FIELD_OPTIONS
234
    const SQL_MODE_POSTGRESQL = 138258;
235
236
    // STRICT_TRANS_TABLES, STRICT_ALL_TABLES, NO_ZERO_IN_DATE, NO_ZERO_DATE,
237
    // ERROR_FOR_DIVISION_BY_ZERO, NO_AUTO_CREATE_USER
238
    const SQL_MODE_TRADITIONAL = 1622052;
239
240
    // -------------------------------------------------------------------------
241
    // Keyword.
242
243
    /**
244
     * Checks if the given string is a keyword.
245
     *
246
     * @param string $str        string to be checked
247
     * @param bool   $isReserved checks if the keyword is reserved
248
     *
249
     * @return int
250
     */
251 365
    public static function isKeyword($str, $isReserved = false)
252
    {
253 365
        $str = strtoupper($str);
254
255 365
        if (isset(static::$KEYWORDS[$str])) {
256 349
            if ($isReserved) {
257 1
                if (!(static::$KEYWORDS[$str] & Token::FLAG_KEYWORD_RESERVED)) {
258 1
                    return null;
259
                }
260
            }
261
262 349
            return static::$KEYWORDS[$str];
263
        }
264
265 362
        return null;
266
    }
267
268
    // -------------------------------------------------------------------------
269
    // Operator.
270
271
    /**
272
     * Checks if the given string is an operator.
273
     *
274
     * @param string $str string to be checked
275
     *
276
     * @return int the appropriate flag for the operator
277
     */
278 374
    public static function isOperator($str)
279
    {
280 374
        if (!isset(static::$OPERATORS[$str])) {
281 372
            return null;
282
        }
283
284 274
        return static::$OPERATORS[$str];
285
    }
286
287
    // -------------------------------------------------------------------------
288
    // Whitespace.
289
290
    /**
291
     * Checks if the given character is a whitespace.
292
     *
293
     * @param string $str string to be checked
294
     *
295
     * @return bool
296
     */
297 379
    public static function isWhitespace($str)
298
    {
299 379
        return ($str === ' ') || ($str === "\r") || ($str === "\n") || ($str === "\t");
300
    }
301
302
    // -------------------------------------------------------------------------
303
    // Comment.
304
305
    /**
306
     * Checks if the given string is the beginning of a whitespace.
307
     *
308
     * @param string $str string to be checked
309
     *
310
     * @return int the appropriate flag for the comment type
311
     */
312 374
    public static function isComment($str, $end=false)
313
    {
314 374
        $len = strlen($str);
315 374
        if ($len == 0) {
316
            return null;
317
        }
318 374
        if ($str[0] === '#') {
319 4
            return Token::FLAG_COMMENT_BASH;
320 374
        } elseif (($len > 1) && ($str[0] === '/') && ($str[1] === '*')) {
321 24
            return (($len > 2) && ($str[2] == '!')) ?
322 24
                Token::FLAG_COMMENT_MYSQL_CMD : Token::FLAG_COMMENT_C;
323 374
        } elseif (($len > 1) && ($str[0] === '*') && ($str[1] === '/')) {
324 3
            return Token::FLAG_COMMENT_C;
325 374
        } elseif (($len > 2) && ($str[0] === '-')
326 7
            && ($str[1] === '-') && (static::isWhitespace($str[2]))
327
        ) {
328 7
            return Token::FLAG_COMMENT_SQL;
329 374
        } elseif (($len == 2) && $end && ($str[0] === '-') && ($str[1] === '-')) {
330 1
            return Token::FLAG_COMMENT_SQL;
331
        }
332
333 374
        return null;
334
    }
335
336
    // -------------------------------------------------------------------------
337
    // Bool.
338
339
    /**
340
     * Checks if the given string is a boolean value.
341
     * This actually check only for `TRUE` and `FALSE` because `1` or `0` are
342
     * actually numbers and are parsed by specific methods.
343
     *
344
     * @param string $str string to be checked
345
     *
346
     * @return bool
347
     */
348 365
    public static function isBool($str)
349
    {
350 365
        $str = strtoupper($str);
351
352 365
        return ($str === 'TRUE') || ($str === 'FALSE');
353
    }
354
355
    // -------------------------------------------------------------------------
356
    // Number.
357
358
    /**
359
     * Checks if the given character can be a part of a number.
360
     *
361
     * @param string $str string to be checked
362
     *
363
     * @return bool
364
     */
365 1
    public static function isNumber($str)
366
    {
367 1
        return (($str >= '0') && ($str <= '9')) || ($str === '.')
368 1
            || ($str === '-') || ($str === '+') || ($str === 'e') || ($str === 'E');
369
    }
370
371
    // -------------------------------------------------------------------------
372
    // Symbol.
373
374
    /**
375
     * Checks if the given character is the beginning of a symbol. A symbol
376
     * can be either a variable or a field name.
377
     *
378
     * @param string $str string to be checked
379
     *
380
     * @return int the appropriate flag for the symbol type
381
     */
382 365 View Code Duplication
    public static function isSymbol($str)
383
    {
384 365
        if (strlen($str) == 0) {
385
            return null;
386
        }
387 365
        if ($str[0] === '@') {
388 29
            return Token::FLAG_SYMBOL_VARIABLE;
389 364
        } elseif ($str[0] === '`') {
390 77
            return Token::FLAG_SYMBOL_BACKTICK;
391
        }
392
393 364
        return null;
394
    }
395
396
    // -------------------------------------------------------------------------
397
    // String.
398
399
    /**
400
     * Checks if the given character is the beginning of a string.
401
     *
402
     * @param string $str string to be checked
403
     *
404
     * @return int the appropriate flag for the string type
405
     */
406 365 View Code Duplication
    public static function isString($str)
407
    {
408 365
        if (strlen($str) == 0) {
409
            return null;
410
        }
411 365
        if ($str[0] === '\'') {
412 68
            return Token::FLAG_STRING_SINGLE_QUOTES;
413 365
        } elseif ($str[0] === '"') {
414 49
            return Token::FLAG_STRING_DOUBLE_QUOTES;
415
        }
416
417 365
        return null;
418
    }
419
420
    // -------------------------------------------------------------------------
421
    // Delimiter.
422
423
    /**
424
     * Checks if the given character can be a separator for two lexeme.
425
     *
426
     * @param string $str string to be checked
427
     *
428
     * @return bool
429
     */
430 365
    public static function isSeparator($str)
431
    {
432
        // NOTES:   Only non alphanumeric ASCII characters may be separators.
433
        //          `~` is the last printable ASCII character.
434 365
        return ($str <= '~') && ($str !== '_')
435 365
            && (($str < '0') || ($str > '9'))
436 365
            && (($str < 'a') || ($str > 'z'))
437 365
            && (($str < 'A') || ($str > 'Z'));
438
    }
439
440
    /**
441
     * Loads the specified context.
442
     *
443
     * Contexts may be used by accessing the context directly.
444
     *
445
     * @param string $context name of the context or full class name that
446
     *                        defines the context
447
     *
448
     * @throws LoaderException if the specified context doesn't exist
449
     */
450 2
    public static function load($context = '')
451
    {
452 2
        if (empty($context)) {
453 1
            $context = self::$defaultContext;
454
        }
455 2
        if ($context[0] !== '\\') {
456
            // Short context name (must be formatted into class name).
457 2
            $context = self::$contextPrefix . $context;
458
        }
459 2
        if (!class_exists($context)) {
460 2
            throw @new LoaderException(
461 2
                'Specified context ("' . $context . '") does not exist.',
462 2
                $context
463
            );
464
        }
465 1
        self::$loadedContext = $context;
466 1
        self::$KEYWORDS = $context::$KEYWORDS;
467 1
    }
468
469
    /**
470
     * Loads the context with the closest version to the one specified.
471
     *
472
     * The closest context is found by replacing last digits with zero until one
473
     * is loaded successfully.
474
     *
475
     * @see Context::load()
476
     *
477
     * @param string $context name of the context or full class name that
478
     *                        defines the context
479
     *
480
     * @return string The loaded context. `null` if no context was loaded.
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

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...
481
     */
482 1
    public static function loadClosest($context = '')
483
    {
484
        /**
485
         * The number of replaces done by `preg_replace`.
486
         * This actually represents whether a new context was generated or not.
487
         *
488
         * @var int
489
         */
490 1
        $count = 0;
491
492
        // As long as a new context can be generated, we try to load it.
493
        do {
494
            try {
495
                // Trying to load the new context.
496 1
                static::load($context);
497 1
            } catch (LoaderException $e) {
498
                // If it didn't work, we are looking for a new one and skipping
499
                // over to the next generation that will try the new context.
500 1
                $context = preg_replace(
501 1
                    '/[1-9](0*)$/',
502 1
                    '0$1',
503 1
                    $context,
504 1
                    -1,
505 1
                    $count
506
                );
507 1
                continue;
508
            }
509
510
            // Last generated context was valid (did not throw any exceptions).
511
            // So we return it, to let the user know what context was loaded.
512 1
            return $context;
513 1
        } while ($count !== 0);
514
515 1
        return null;
516
    }
517
518
    /**
519
     * Sets the SQL mode.
520
     *
521
     * @param string $mode The list of modes. If empty, the mode is reset.
522
     */
523 2
    public static function setMode($mode = '')
524
    {
525 2
        static::$MODE = 0;
526 2
        if (empty($mode)) {
527 2
            return;
528
        }
529 2
        $mode = explode(',', $mode);
530 2
        foreach ($mode as $m) {
531 2
            static::$MODE |= constant('static::SQL_MODE_' . $m);
532
        }
533 2
    }
534
535
    /**
536
     * Escapes the symbol by adding surrounding backticks.
537
     *
538
     * @param array|string $str   the string to be escaped
539
     * @param string       $quote quote to be used when escaping
540
     *
541
     * @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...
542
     */
543 19
    public static function escape($str, $quote = '`')
544
    {
545 19
        if (is_array($str)) {
546 11
            foreach ($str as $key => $value) {
547 11
                $str[$key] = static::escape($value);
548
            }
549
550 11
            return $str;
551
        }
552
553 19
        if ((static::$MODE & self::SQL_MODE_NO_ENCLOSING_QUOTES)
554 1
            && (!static::isKeyword($str, true))
555
        ) {
556 1
            return $str;
557
        }
558
559 19
        if (static::$MODE & self::SQL_MODE_ANSI_QUOTES) {
560 1
            $quote = '"';
561
        }
562
563 19
        return $quote . str_replace($quote, $quote . $quote, $str) . $quote;
564
    }
565
}
566
567
// Initializing the default context.
568
Context::load();
569