phpmyadmin /
sql-parser
| 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
Loading history...
|
|||
| 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
Loading history...
|
|||
| 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 |