phpmyadmin /
sql-parser
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace PhpMyAdmin\SqlParser\Parsers; |
||
| 6 | |||
| 7 | use AllowDynamicProperties; |
||
|
0 ignored issues
–
show
|
|||
| 8 | use PhpMyAdmin\SqlParser\Components\Expression; |
||
| 9 | use PhpMyAdmin\SqlParser\Exceptions\ParserException; |
||
| 10 | use PhpMyAdmin\SqlParser\Parseable; |
||
| 11 | use PhpMyAdmin\SqlParser\Parser; |
||
| 12 | use PhpMyAdmin\SqlParser\Token; |
||
| 13 | use PhpMyAdmin\SqlParser\TokensList; |
||
| 14 | use PhpMyAdmin\SqlParser\TokenType; |
||
| 15 | |||
| 16 | use function implode; |
||
| 17 | use function in_array; |
||
| 18 | use function rtrim; |
||
| 19 | use function strlen; |
||
| 20 | use function trim; |
||
| 21 | |||
| 22 | /** |
||
| 23 | * Parses a reference to an expression (column, table or database name, function |
||
| 24 | * call, mathematical expression, etc.). |
||
| 25 | */ |
||
| 26 | #[AllowDynamicProperties] |
||
| 27 | final class Expressions implements Parseable |
||
| 28 | { |
||
| 29 | /** |
||
| 30 | * List of allowed reserved keywords in expressions. |
||
| 31 | */ |
||
| 32 | private const ALLOWED_KEYWORDS = [ |
||
| 33 | 'AND', |
||
| 34 | 'AS', |
||
| 35 | 'BETWEEN', |
||
| 36 | 'CASE', |
||
| 37 | 'DUAL', |
||
| 38 | 'DIV', |
||
| 39 | 'IS', |
||
| 40 | 'MOD', |
||
| 41 | 'NOT', |
||
| 42 | 'NOT NULL', |
||
| 43 | 'NULL', |
||
| 44 | 'OR', |
||
| 45 | 'OVER', |
||
| 46 | 'REGEXP', |
||
| 47 | 'RLIKE', |
||
| 48 | 'XOR', |
||
| 49 | ]; |
||
| 50 | |||
| 51 | /** |
||
| 52 | * Possible options:. |
||
| 53 | * |
||
| 54 | * `field` |
||
| 55 | * |
||
| 56 | * First field to be filled. |
||
| 57 | * If this is not specified, it takes the value of `parseField`. |
||
| 58 | * |
||
| 59 | * `parseField` |
||
| 60 | * |
||
| 61 | * Specifies the type of the field parsed. It may be `database`, |
||
| 62 | * `table` or `column`. These expressions may not include |
||
| 63 | * parentheses. |
||
| 64 | * |
||
| 65 | * `breakOnAlias` |
||
| 66 | * |
||
| 67 | * If not empty, breaks when the alias occurs (it is not included). |
||
| 68 | * |
||
| 69 | * `breakOnParentheses` |
||
| 70 | * |
||
| 71 | * If not empty, breaks when the first parentheses occurs. |
||
| 72 | * |
||
| 73 | * `parenthesesDelimited` |
||
| 74 | * |
||
| 75 | * If not empty, breaks after last parentheses occurred. |
||
| 76 | * |
||
| 77 | * @param Parser $parser the parser that serves as context |
||
| 78 | * @param TokensList $list the list of tokens that are being parsed |
||
| 79 | * @param array<string, mixed> $options parameters for parsing |
||
| 80 | * |
||
| 81 | * @throws ParserException |
||
| 82 | */ |
||
| 83 | 1158 | public static function parse(Parser $parser, TokensList $list, array $options = []): Expression|null |
|
| 84 | { |
||
| 85 | 1158 | $ret = new Expression(); |
|
| 86 | |||
| 87 | /** |
||
| 88 | * Whether current tokens make an expression or a table reference. |
||
| 89 | */ |
||
| 90 | 1158 | $isExpr = false; |
|
| 91 | |||
| 92 | /** |
||
| 93 | * Whether a period was previously found. |
||
| 94 | */ |
||
| 95 | 1158 | $dot = false; |
|
| 96 | |||
| 97 | /** |
||
| 98 | * Whether an alias is expected. Is 2 if `AS` keyword was found. |
||
| 99 | */ |
||
| 100 | 1158 | $alias = false; |
|
| 101 | |||
| 102 | /** |
||
| 103 | * Counts brackets. |
||
| 104 | */ |
||
| 105 | 1158 | $brackets = 0; |
|
| 106 | |||
| 107 | /** |
||
| 108 | * Keeps track of the last two previous tokens. |
||
| 109 | */ |
||
| 110 | 1158 | $prev = [ |
|
| 111 | 1158 | null, |
|
| 112 | 1158 | null, |
|
| 113 | 1158 | ]; |
|
| 114 | |||
| 115 | // When a field is parsed, no parentheses are expected. |
||
| 116 | 1158 | if (! empty($options['parseField'])) { |
|
| 117 | 688 | $options['breakOnParentheses'] = true; |
|
| 118 | 688 | $options['field'] = $options['parseField']; |
|
| 119 | } |
||
| 120 | |||
| 121 | 1158 | for (; $list->idx < $list->count; ++$list->idx) { |
|
| 122 | /** |
||
| 123 | * Token parsed at this moment. |
||
| 124 | */ |
||
| 125 | 1158 | $token = $list->tokens[$list->idx]; |
|
| 126 | |||
| 127 | // End of statement. |
||
| 128 | 1158 | if ($token->type === TokenType::Delimiter) { |
|
| 129 | 430 | break; |
|
| 130 | } |
||
| 131 | |||
| 132 | // Skipping whitespaces and comments. |
||
| 133 | 1156 | if (($token->type === TokenType::Whitespace) || ($token->type === TokenType::Comment)) { |
|
| 134 | // If the token is a closing C comment from a MySQL command, it must be ignored. |
||
| 135 | 996 | if ($isExpr && $token->token !== '*/') { |
|
| 136 | 412 | $ret->expr .= $token->token; |
|
| 137 | } |
||
| 138 | |||
| 139 | 996 | continue; |
|
| 140 | } |
||
| 141 | |||
| 142 | 1156 | if ($token->type === TokenType::Keyword) { |
|
| 143 | 914 | if (($brackets > 0) && empty($ret->subquery) && ! empty(Parser::STATEMENT_PARSERS[$token->keyword])) { |
|
| 144 | // A `(` was previously found and this keyword is the |
||
| 145 | // beginning of a statement, so this is a subquery. |
||
| 146 | 68 | $ret->subquery = $token->keyword; |
|
| 147 | } elseif ( |
||
| 148 | 912 | ($token->flags & Token::FLAG_KEYWORD_FUNCTION) |
|
| 149 | 912 | && (empty($options['parseField']) |
|
| 150 | 912 | && ! $alias) |
|
| 151 | ) { |
||
| 152 | 106 | $isExpr = true; |
|
| 153 | 888 | } elseif (($token->flags & Token::FLAG_KEYWORD_RESERVED) && ($brackets === 0)) { |
|
| 154 | 786 | if (! in_array($token->keyword, self::ALLOWED_KEYWORDS, true)) { |
|
| 155 | // A reserved keyword that is not allowed in the |
||
| 156 | // expression was found so the expression must have |
||
| 157 | // ended and a new clause is starting. |
||
| 158 | 736 | break; |
|
| 159 | } |
||
| 160 | |||
| 161 | 208 | if ($token->keyword === 'AS') { |
|
| 162 | 160 | if (! empty($options['breakOnAlias'])) { |
|
| 163 | 30 | break; |
|
| 164 | } |
||
| 165 | |||
| 166 | 146 | if ($alias) { |
|
| 167 | 2 | $parser->error('An alias was expected.', $token); |
|
| 168 | 2 | break; |
|
| 169 | } |
||
| 170 | |||
| 171 | 146 | $alias = true; |
|
| 172 | 146 | continue; |
|
| 173 | } |
||
| 174 | |||
| 175 | 64 | if ($token->keyword === 'CASE') { |
|
| 176 | // For a use of CASE like |
||
| 177 | // 'SELECT a = CASE .... END, b=1, `id`, ... FROM ...' |
||
| 178 | 10 | $tempCaseExpr = CaseExpressions::parse($parser, $list); |
|
| 179 | 10 | $ret->expr .= $tempCaseExpr->build(); |
|
| 180 | 10 | $isExpr = true; |
|
| 181 | 10 | continue; |
|
| 182 | } |
||
| 183 | |||
| 184 | 54 | $isExpr = true; |
|
| 185 | } elseif ( |
||
| 186 | 280 | $brackets === 0 && strlen((string) $ret->expr) > 0 && ! $alias |
|
| 187 | 280 | && ($ret->table === null || $ret->table === '') |
|
| 188 | ) { |
||
| 189 | /* End of expression */ |
||
| 190 | 110 | break; |
|
| 191 | } |
||
| 192 | } |
||
| 193 | |||
| 194 | if ( |
||
| 195 | 1152 | ($token->type === TokenType::Number) |
|
| 196 | 1128 | || ($token->type === TokenType::Bool) |
|
| 197 | 1128 | || (($token->type === TokenType::Symbol) |
|
| 198 | 1128 | && ($token->flags & Token::FLAG_SYMBOL_VARIABLE)) |
|
| 199 | 1120 | || (($token->type === TokenType::Symbol) |
|
| 200 | 1120 | && ($token->flags & Token::FLAG_SYMBOL_PARAMETER)) |
|
| 201 | 1152 | || (($token->type === TokenType::Operator) |
|
| 202 | 1152 | && ($token->value !== '.')) |
|
| 203 | ) { |
||
| 204 | 706 | if (! empty($options['parseField'])) { |
|
| 205 | 236 | break; |
|
| 206 | } |
||
| 207 | |||
| 208 | // Numbers, booleans and operators (except dot) are usually part |
||
| 209 | // of expressions. |
||
| 210 | 552 | $isExpr = true; |
|
| 211 | } |
||
| 212 | |||
| 213 | 1152 | if ($token->type === TokenType::Operator) { |
|
| 214 | 488 | if (! empty($options['breakOnParentheses']) && (($token->value === '(') || ($token->value === ')'))) { |
|
| 215 | // No brackets were expected. |
||
| 216 | 4 | break; |
|
| 217 | } |
||
| 218 | |||
| 219 | 486 | if ($token->value === '(') { |
|
| 220 | 182 | ++$brackets; |
|
| 221 | if ( |
||
| 222 | 182 | empty($ret->function) && ($prev[1] !== null) |
|
| 223 | 182 | && (($prev[1]->type === TokenType::None) |
|
| 224 | 182 | || ($prev[1]->type === TokenType::Symbol) |
|
| 225 | 182 | || (($prev[1]->type === TokenType::Keyword) |
|
| 226 | 182 | && ($prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION))) |
|
| 227 | ) { |
||
| 228 | 58 | $ret->function = $prev[1]->value; |
|
| 229 | } |
||
| 230 | 484 | } elseif ($token->value === ')') { |
|
| 231 | 196 | if ($brackets === 0) { |
|
| 232 | // Not our bracket |
||
| 233 | 22 | break; |
|
| 234 | } |
||
| 235 | |||
| 236 | 180 | --$brackets; |
|
| 237 | 180 | if ($brackets === 0) { |
|
| 238 | 180 | if (! empty($options['parenthesesDelimited'])) { |
|
| 239 | // The current token is the last bracket, the next |
||
| 240 | // one will be outside the expression. |
||
| 241 | 50 | $ret->expr .= $token->token; |
|
| 242 | 50 | ++$list->idx; |
|
| 243 | 50 | break; |
|
| 244 | } |
||
| 245 | 22 | } elseif ($brackets < 0) { |
|
| 246 | // $parser->error('Unexpected closing bracket.', $token); |
||
| 247 | // $brackets = 0; |
||
| 248 | break; |
||
| 249 | } |
||
| 250 | 432 | } elseif ($token->value === ',') { |
|
| 251 | // Expressions are comma-delimited. |
||
| 252 | 278 | if ($brackets === 0) { |
|
| 253 | 244 | break; |
|
| 254 | } |
||
| 255 | } |
||
| 256 | } |
||
| 257 | |||
| 258 | // Saving the previous tokens. |
||
| 259 | 1150 | $prev[0] = $prev[1]; |
|
| 260 | 1150 | $prev[1] = $token; |
|
| 261 | |||
| 262 | 1150 | if ($alias) { |
|
| 263 | // An alias is expected (the keyword `AS` was previously found). |
||
| 264 | 144 | if (! empty($ret->alias)) { |
|
| 265 | 2 | $parser->error('An alias was previously found.', $token); |
|
| 266 | 2 | break; |
|
| 267 | } |
||
| 268 | |||
| 269 | 144 | $ret->alias = $token->value; |
|
| 270 | 144 | $alias = false; |
|
| 271 | 1150 | } elseif ($isExpr) { |
|
| 272 | // Handling aliases. |
||
| 273 | if ( |
||
| 274 | 508 | $brackets === 0 |
|
| 275 | 508 | && ($prev[0] === null |
|
| 276 | 508 | || (($prev[0]->type !== TokenType::Operator || $prev[0]->token === ')') |
|
| 277 | 508 | && ($prev[0]->type !== TokenType::Keyword |
|
| 278 | 508 | || ! ($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED)))) |
|
| 279 | 508 | && (($prev[1]->type === TokenType::String) |
|
| 280 | 508 | || ($prev[1]->type === TokenType::Symbol |
|
| 281 | 508 | && ! ($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE) |
|
| 282 | 508 | && ! ($prev[1]->flags & Token::FLAG_SYMBOL_PARAMETER)) |
|
| 283 | 508 | || ($prev[1]->type === TokenType::None |
|
| 284 | 508 | && $prev[1]->token !== 'OVER')) |
|
| 285 | ) { |
||
| 286 | 14 | if (! empty($ret->alias)) { |
|
| 287 | 4 | $parser->error('An alias was previously found.', $token); |
|
| 288 | 4 | break; |
|
| 289 | } |
||
| 290 | |||
| 291 | 12 | $ret->alias = $prev[1]->value; |
|
| 292 | } else { |
||
| 293 | 508 | $currIdx = $list->idx; |
|
| 294 | 508 | --$list->idx; |
|
| 295 | 508 | $beforeToken = $list->getPrevious(); |
|
| 296 | 508 | $list->idx = $currIdx; |
|
| 297 | // columns names tokens are of type NONE, or SYMBOL (`col`), and the columns options |
||
| 298 | // would start with a token of type KEYWORD, in that case, we want to have a space |
||
| 299 | // between the tokens. |
||
| 300 | if ( |
||
| 301 | 508 | $ret->expr !== null && |
|
| 302 | $beforeToken && |
||
| 303 | 508 | ($beforeToken->type === TokenType::None || |
|
| 304 | 508 | $beforeToken->type === TokenType::Symbol || $beforeToken->type === TokenType::String) && |
|
| 305 | 508 | $token->type === TokenType::Keyword |
|
| 306 | ) { |
||
| 307 | 76 | $ret->expr = rtrim($ret->expr, ' ') . ' '; |
|
| 308 | } |
||
| 309 | |||
| 310 | 508 | $ret->expr .= $token->token; |
|
| 311 | } |
||
| 312 | } else { |
||
| 313 | 1060 | if (($token->type === TokenType::Operator) && ($token->value === '.')) { |
|
| 314 | // Found a `.` which means we expect a column name and |
||
| 315 | // the column name we parsed is actually the table name |
||
| 316 | // and the table name is actually a database name. |
||
| 317 | 112 | if (! empty($ret->database) || $dot) { |
|
| 318 | 4 | $parser->error('Unexpected dot.', $token); |
|
| 319 | } |
||
| 320 | |||
| 321 | 112 | $ret->database = $ret->table; |
|
| 322 | 112 | $ret->table = $ret->column; |
|
| 323 | 112 | $ret->column = null; |
|
| 324 | 112 | $dot = true; |
|
| 325 | 112 | $ret->expr .= $token->token; |
|
| 326 | } else { |
||
| 327 | 1060 | $field = empty($options['field']) ? 'column' : $options['field']; |
|
| 328 | 1060 | if (empty($ret->$field)) { |
|
| 329 | 1060 | $ret->$field = $token->value; |
|
| 330 | 1060 | $ret->expr .= $token->token; |
|
| 331 | 1060 | $dot = false; |
|
| 332 | } else { |
||
| 333 | // No alias is expected. |
||
| 334 | 200 | if (! empty($options['breakOnAlias'])) { |
|
| 335 | 140 | break; |
|
| 336 | } |
||
| 337 | |||
| 338 | 60 | if (! empty($ret->alias)) { |
|
| 339 | 12 | $parser->error('An alias was previously found.', $token); |
|
| 340 | 12 | break; |
|
| 341 | } |
||
| 342 | |||
| 343 | 52 | $ret->alias = $token->value; |
|
| 344 | } |
||
| 345 | } |
||
| 346 | } |
||
| 347 | } |
||
| 348 | |||
| 349 | 1158 | if ($alias) { |
|
| 350 | 6 | $parser->error('An alias was expected.', $list->tokens[$list->idx - 1]); |
|
| 351 | } |
||
| 352 | |||
| 353 | // White-spaces might be added at the end. |
||
| 354 | 1158 | $ret->expr = trim((string) $ret->expr); |
|
| 355 | |||
| 356 | 1158 | if ($ret->expr === '') { |
|
| 357 | 58 | return null; |
|
| 358 | } |
||
| 359 | |||
| 360 | 1150 | --$list->idx; |
|
| 361 | |||
| 362 | 1150 | return $ret; |
|
| 363 | } |
||
| 364 | |||
| 365 | /** @param Expression[] $component the component to be built */ |
||
| 366 | 14 | public static function buildAll(array $component): string |
|
| 367 | { |
||
| 368 | 14 | return implode(', ', $component); |
|
| 369 | } |
||
| 370 | } |
||
| 371 |
The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g.
excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths