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