Completed
Pull Request — master (#173)
by Madhura
07:06
created

Context::isSeparator()   B

Complexity

Conditions 8
Paths 23

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 8

Importance

Changes 0
Metric Value
cc 8
eloc 5
nc 23
nop 1
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 8
rs 7.7777
c 0
b 0
f 0
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
    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 364
        } elseif ($str[0] === ':') {
392
            return Token::FLAG_SYMBOL_PARAMETER;
393
        }
394
395 364
        return null;
396
    }
397
398
    // -------------------------------------------------------------------------
399
    // String.
400
401
    /**
402
     * Checks if the given character is the beginning of a string.
403
     *
404
     * @param string $str string to be checked
405
     *
406
     * @return int the appropriate flag for the string type
407
     */
408 365
    public static function isString($str)
409
    {
410 365
        if (strlen($str) == 0) {
411
            return null;
412
        }
413 365
        if ($str[0] === '\'') {
414 68
            return Token::FLAG_STRING_SINGLE_QUOTES;
415 365
        } elseif ($str[0] === '"') {
416 49
            return Token::FLAG_STRING_DOUBLE_QUOTES;
417
        }
418
419 365
        return null;
420
    }
421
422
    // -------------------------------------------------------------------------
423
    // Delimiter.
424
425
    /**
426
     * Checks if the given character can be a separator for two lexeme.
427
     *
428
     * @param string $str string to be checked
429
     *
430
     * @return bool
431
     */
432 365
    public static function isSeparator($str)
433
    {
434
        // NOTES:   Only non alphanumeric ASCII characters may be separators.
435
        //          `~` is the last printable ASCII character.
436 365
        return ($str <= '~') && ($str !== '_')
437 365
            && (($str < '0') || ($str > '9'))
438 365
            && (($str < 'a') || ($str > 'z'))
439 365
            && (($str < 'A') || ($str > 'Z'));
440
    }
441
442
    /**
443
     * Loads the specified context.
444
     *
445
     * Contexts may be used by accessing the context directly.
446
     *
447
     * @param string $context name of the context or full class name that
448
     *                        defines the context
449
     *
450
     * @throws LoaderException if the specified context doesn't exist
451
     */
452 12
    public static function load($context = '')
453
    {
454 12
        if (empty($context)) {
455 11
            $context = self::$defaultContext;
456
        }
457 12
        if ($context[0] !== '\\') {
458
            // Short context name (must be formatted into class name).
459 12
            $context = self::$contextPrefix . $context;
460
        }
461 12
        if (!class_exists($context)) {
462 2
            throw @new LoaderException(
463 2
                'Specified context ("' . $context . '") does not exist.',
464 2
                $context
465
            );
466
        }
467 11
        self::$loadedContext = $context;
468 11
        self::$KEYWORDS = $context::$KEYWORDS;
469 11
    }
470
471
    /**
472
     * Loads the context with the closest version to the one specified.
473
     *
474
     * The closest context is found by replacing last digits with zero until one
475
     * is loaded successfully.
476
     *
477
     * @see Context::load()
478
     *
479
     * @param string $context name of the context or full class name that
480
     *                        defines the context
481
     *
482
     * @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...
483
     */
484 1
    public static function loadClosest($context = '')
485
    {
486
        /**
487
         * The number of replaces done by `preg_replace`.
488
         * This actually represents whether a new context was generated or not.
489
         *
490
         * @var int
491
         */
492 1
        $count = 0;
493
494
        // As long as a new context can be generated, we try to load it.
495
        do {
496
            try {
497
                // Trying to load the new context.
498 1
                static::load($context);
499 1
            } catch (LoaderException $e) {
500
                // If it didn't work, we are looking for a new one and skipping
501
                // over to the next generation that will try the new context.
502 1
                $context = preg_replace(
503 1
                    '/[1-9](0*)$/',
504 1
                    '0$1',
505 1
                    $context,
506 1
                    -1,
507 1
                    $count
508
                );
509 1
                continue;
510
            }
511
512
            // Last generated context was valid (did not throw any exceptions).
513
            // So we return it, to let the user know what context was loaded.
514 1
            return $context;
515 1
        } while ($count !== 0);
516
517 1
        return null;
518
    }
519
520
    /**
521
     * Sets the SQL mode.
522
     *
523
     * @param string $mode The list of modes. If empty, the mode is reset.
524
     */
525 2
    public static function setMode($mode = '')
526
    {
527 2
        static::$MODE = 0;
528 2
        if (empty($mode)) {
529 2
            return;
530
        }
531 2
        $mode = explode(',', $mode);
532 2
        foreach ($mode as $m) {
533 2
            static::$MODE |= constant('static::SQL_MODE_' . $m);
534
        }
535 2
    }
536
537
    /**
538
     * Escapes the symbol by adding surrounding backticks.
539
     *
540
     * @param array|string $str   the string to be escaped
541
     * @param string       $quote quote to be used when escaping
542
     *
543
     * @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...
544
     */
545 19
    public static function escape($str, $quote = '`')
546
    {
547 19
        if (is_array($str)) {
548 11
            foreach ($str as $key => $value) {
549 11
                $str[$key] = static::escape($value);
550
            }
551
552 11
            return $str;
553
        }
554
555 19
        if ((static::$MODE & self::SQL_MODE_NO_ENCLOSING_QUOTES)
556 1
            && (!static::isKeyword($str, true))
557
        ) {
558 1
            return $str;
559
        }
560
561 19
        if (static::$MODE & self::SQL_MODE_ANSI_QUOTES) {
562 1
            $quote = '"';
563
        }
564
565 19
        return $quote . str_replace($quote, $quote . $quote, $str) . $quote;
566
    }
567
}
568
569
// Initializing the default context.
570
Context::load();
571