| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* MySQL token. |
| 5 |
* |
| 6 |
* This class represents a MySQL SQL token that is produced by WP_MySQL_Lexer, |
| 7 |
* and consumed by WP_MySQL_Parser during the parsing process. |
| 8 |
* |
| 9 |
* @access private |
| 10 |
*/ |
| 11 |
class WP_MySQL_Token extends WP_Parser_Token { |
| 12 |
/** |
| 13 |
* Whether the NO_BACKSLASH_ESCAPES SQL mode is enabled. |
| 14 |
* |
| 15 |
* @var bool |
| 16 |
*/ |
| 17 |
private $sql_mode_no_backslash_escapes_enabled; |
| 18 |
|
| 19 |
/** |
| 20 |
* Constructor. |
| 21 |
* |
| 22 |
* @param int $id Token type. |
| 23 |
* @param int $start Byte offset in the input where the token begins. |
| 24 |
* @param int $length Byte length of the token in the input. |
| 25 |
* @param string $input Input bytes from which the token was parsed. |
| 26 |
* @param bool $sql_mode_no_backslash_escapes_enabled Whether the NO_BACKSLASH_ESCAPES SQL mode is enabled. |
| 27 |
*/ |
| 28 |
public function __construct( |
| 29 |
int $id, |
| 30 |
int $start, |
| 31 |
int $length, |
| 32 |
string $input, |
| 33 |
bool $sql_mode_no_backslash_escapes_enabled |
| 34 |
) { |
| 35 |
$this->id = $id; |
| 36 |
$this->start = $start; |
| 37 |
$this->length = $length; |
| 38 |
$this->input = $input; |
| 39 |
|
| 40 |
$this->sql_mode_no_backslash_escapes_enabled = $sql_mode_no_backslash_escapes_enabled; |
| 41 |
} |
| 42 |
|
| 43 |
/** |
| 44 |
* Get the name of the token. |
| 45 |
* |
| 46 |
* This method is intended to be used only for testing and debugging purposes, |
| 47 |
* when tokens need to be presented by their names in a human-readable form. |
| 48 |
* It should not be used in production code, as it's not performance-optimized. |
| 49 |
* |
| 50 |
* @return string The token name. |
| 51 |
*/ |
| 52 |
public function get_name(): string { |
| 53 |
$name = WP_MySQL_Lexer::get_token_name( $this->id ); |
| 54 |
if ( null === $name ) { |
| 55 |
$name = 'UNKNOWN'; |
| 56 |
} |
| 57 |
return $name; |
| 58 |
} |
| 59 |
|
| 60 |
/** |
| 61 |
* Get the real unquoted value of the token. |
| 62 |
* |
| 63 |
* @return string The token value. |
| 64 |
*/ |
| 65 |
public function get_value(): string { |
| 66 |
$value = $this->get_bytes(); |
| 67 |
if ( |
| 68 |
WP_MySQL_Lexer::SINGLE_QUOTED_TEXT === $this->id |
| 69 |
|| WP_MySQL_Lexer::DOUBLE_QUOTED_TEXT === $this->id |
| 70 |
|| WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $this->id |
| 71 |
) { |
| 72 |
// Remove bounding quotes. |
| 73 |
$quote = $value[0]; |
| 74 |
$value = substr( $value, 1, -1 ); |
| 75 |
|
| 76 |
/* |
| 77 |
* Quoted identifiers escape their bounding quote by doubling it, while |
| 78 |
* backslashes remain literal. Strings in NO_BACKSLASH_ESCAPES mode use |
| 79 |
* the same unquoting rule. |
| 80 |
* |
| 81 |
* See: https://dev.mysql.com/doc/refman/8.4/en/identifiers.html |
| 82 |
*/ |
| 83 |
if ( |
| 84 |
WP_MySQL_Lexer::BACK_TICK_QUOTED_ID === $this->id |
| 85 |
|| $this->sql_mode_no_backslash_escapes_enabled |
| 86 |
) { |
| 87 |
return str_replace( $quote . $quote, $quote, $value ); |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Unescape MySQL escape sequences. |
| 92 |
* |
| 93 |
* MySQL string literals use backslash as an escape character, and |
| 94 |
* the string bounding quotes can also be escaped by being doubled. |
| 95 |
* |
| 96 |
* The escaping is done according to the following rules: |
| 97 |
* |
| 98 |
* 1. Some special character escape sequences are recognized. |
| 99 |
* For example, "\n" is a newline character, "\0" is ASCII NULL. |
| 100 |
* 2. A specific treatment is applied to "\%" and "\_" sequences. |
| 101 |
* This is due to their special meaning for pattern matching. |
| 102 |
* 3. Other backslash-prefixed characters resolve to their literal |
| 103 |
* values. For example, "\x" represents "x", "\\" represents "\". |
| 104 |
* |
| 105 |
* Despite looking similar, these rules are different from the C-style |
| 106 |
* string escaping, so we cannot use "strip(c)slashes()" in this case. |
| 107 |
* |
| 108 |
* See: https://dev.mysql.com/doc/refman/8.4/en/string-literals.html |
| 109 |
*/ |
| 110 |
$backslash = chr( 92 ); |
| 111 |
$replacements = array( |
| 112 |
/* |
| 113 |
* MySQL special character escape sequences. |
| 114 |
*/ |
| 115 |
( $backslash . '0' ) => chr( 0 ), // An ASCII NULL character (\0). |
| 116 |
( $backslash . "'" ) => chr( 39 ), // A single quote character ('). |
| 117 |
( $backslash . '"' ) => chr( 34 ), // A double quote character ("). |
| 118 |
( $backslash . 'b' ) => chr( 8 ), // A backspace character. |
| 119 |
( $backslash . 'n' ) => chr( 10 ), // A newline (linefeed) character (\n). |
| 120 |
( $backslash . 'r' ) => chr( 13 ), // A carriage return character (\r). |
| 121 |
( $backslash . 't' ) => chr( 9 ), // A tab character (\t). |
| 122 |
( $backslash . 'Z' ) => chr( 26 ), // An ASCII 26 (Control+Z) character. |
| 123 |
|
| 124 |
/* |
| 125 |
* Normalize escaping of "%" and "_" characters. |
| 126 |
* |
| 127 |
* MySQL has unusual handling for "\%" and "\_" in all string literals. |
| 128 |
* While other sequences follow the C-style escaping ("\?" is "?", etc.), |
| 129 |
* "\%" resolves to "\%" and "\_" resolves to "\_" (unlike in C strings). |
| 130 |
* |
| 131 |
* This means that "\%" behaves like "\\%", and "\_" behaves like "\\_". |
| 132 |
* To preserve this behavior, we need to add a second backslash here. |
| 133 |
* |
| 134 |
* From https://dev.mysql.com/doc/refman/8.4/en/string-literals.html: |
| 135 |
* > The \% and \_ sequences are used to search for literal instances |
| 136 |
* > of % and _ in pattern-matching contexts where they would otherwise |
| 137 |
* > be interpreted as wildcard characters. If you use \% or \_ outside |
| 138 |
* > of pattern-matching contexts, they evaluate to the strings \% and |
| 139 |
* > \_, not to % and _. |
| 140 |
*/ |
| 141 |
( $backslash . '%' ) => $backslash . $backslash . '%', |
| 142 |
( $backslash . '_' ) => $backslash . $backslash . '_', |
| 143 |
|
| 144 |
/* |
| 145 |
* Preserve a double backslash as-is, so that the trailing backslash |
| 146 |
* is not consumed as the beginning of an escape sequence like "\n". |
| 147 |
* |
| 148 |
* Resolving "\\" to "\" will be handled in the next step, where all |
| 149 |
* other backslash-prefixed characters resolve to their literal values. |
| 150 |
*/ |
| 151 |
( $backslash . $backslash ) |
| 152 |
=> $backslash . $backslash, |
| 153 |
|
| 154 |
/* |
| 155 |
* The bounding quotes can also be escaped by being doubled. |
| 156 |
*/ |
| 157 |
( $quote . $quote ) => $quote, |
| 158 |
); |
| 159 |
|
| 160 |
/* |
| 161 |
* Apply the replacements. |
| 162 |
* |
| 163 |
* It is important to use "strtr()" and not "str_replace()", because |
| 164 |
* "str_replace()" applies replacements one after another, modifying |
| 165 |
* intermediate changes rather than just the original string: |
| 166 |
* |
| 167 |
* - str_replace( [ 'a', 'b' ], [ 'b', 'c' ], 'ab' ); // 'cc' (bad) |
| 168 |
* - strtr( 'ab', [ 'a' => 'b', 'b' => 'c' ] ); // 'bc' (good) |
| 169 |
*/ |
| 170 |
$value = strtr( $value, $replacements ); |
| 171 |
|
| 172 |
/* |
| 173 |
* A backslash with any other character represents the character itself. |
| 174 |
* That is, \x evaluates to x, \\ evaluates to \, and \🙂 evaluates to 🙂. |
| 175 |
* |
| 176 |
* Use "s" (DOTALL) to match all characters including newlines. Do not use |
| 177 |
* "u", so non-UTF-8 bytes are preserved (binary or single-byte charsets). |
| 178 |
* Legacy multi-byte charsets like big5 or gbk, where a trail byte can be |
| 179 |
* a backslash, are not supported. |
| 180 |
*/ |
| 181 |
$preg_quoted_backslash = preg_quote( $backslash ); |
| 182 |
$value = preg_replace( "/$preg_quoted_backslash(.)/s", '$1', $value ); |
| 183 |
} |
| 184 |
return $value; |
| 185 |
} |
| 186 |
|
| 187 |
/** |
| 188 |
* Get the token representation as a string. |
| 189 |
* |
| 190 |
* This method is intended to be used only for testing and debugging purposes, |
| 191 |
* when tokens need to be presented in a human-readable form. It should not |
| 192 |
* be used in production code, as it's not performance-optimized. |
| 193 |
* |
| 194 |
* @return string |
| 195 |
*/ |
| 196 |
public function __toString(): string { |
| 197 |
return $this->get_value() . '<' . $this->id . ',' . $this->get_name() . '>'; |
| 198 |
} |
| 199 |
} |
| 200 |
|