diff --git a/src/xsprintf/qsprintf.php b/src/xsprintf/qsprintf.php index 14f83be..206cbba 100644 --- a/src/xsprintf/qsprintf.php +++ b/src/xsprintf/qsprintf.php @@ -1,474 +1,514 @@ and %<. * * %> ("Prefix") * Escapes a prefix query for a LIKE clause. For example: * * // Find all rows where `name` starts with $prefix. * qsprintf($escaper, 'WHERE name LIKE %>', $prefix); * * %< ("Suffix") * Escapes a suffix query for a LIKE clause. For example: * * // Find all rows where `name` ends with $suffix. * qsprintf($escaper, 'WHERE name LIKE %<', $suffix); * * %T ("Table") * Escapes a table name. In most cases, you should use "%R" instead. */ function qsprintf(PhutilQsprintfInterface $escaper, $pattern /* , ... */) { $args = func_get_args(); array_shift($args); return new PhutilQueryString($escaper, $args); } function vqsprintf(PhutilQsprintfInterface $escaper, $pattern, array $argv) { array_unshift($argv, $pattern); return new PhutilQueryString($escaper, $argv); } /** * @{function:xsprintf} callback for encoding SQL queries. See * @{function:qsprintf}. */ function xsprintf_query($userdata, &$pattern, &$pos, &$value, &$length) { $type = $pattern[$pos]; if (is_array($userdata)) { $escaper = $userdata['escaper']; $unmasked = $userdata['unmasked']; } else { $escaper = $userdata; $unmasked = false; } $next = (strlen($pattern) > $pos + 1) ? $pattern[$pos + 1] : null; $nullable = false; $done = false; $prefix = ''; if (!($escaper instanceof PhutilQsprintfInterface)) { throw new InvalidArgumentException(pht('Invalid database escaper.')); } switch ($type) { case '=': // Nullable test switch ($next) { case 'd': case 'f': case 's': $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = 's'; if ($value === null) { $value = 'IS NULL'; $done = true; } else { $prefix = '= '; $type = $next; } break; default: throw new Exception( pht( 'Unknown conversion, try %s, %s, or %s.', '%=d', '%=s', '%=f')); } break; case 'n': // Nullable... switch ($next) { case 'd': // ...integer. case 'f': // ...float. case 's': // ...string. case 'B': // ...binary string. $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = $next; $nullable = true; break; default: throw new XsprintfUnknownConversionException("%n{$next}"); } break; case 'L': // List of.. qsprintf_check_type($value, "L{$next}", $pattern); $pattern = substr_replace($pattern, '', $pos, 1); $length = strlen($pattern); $type = 's'; $done = true; switch ($next) { case 'd': // ...integers. $value = implode(', ', array_map('intval', $value)); break; case 'f': // ...floats. $value = implode(', ', array_map('floatval', $value)); break; case 's': // ...strings. foreach ($value as $k => $v) { $value[$k] = "'".$escaper->escapeUTF8String((string)$v)."'"; } $value = implode(', ', $value); break; case 'B': // ...binary strings. foreach ($value as $k => $v) { $value[$k] = "'".$escaper->escapeBinaryString((string)$v)."'"; } $value = implode(', ', $value); break; case 'C': // ...columns. foreach ($value as $k => $v) { $value[$k] = $escaper->escapeColumnName($v); } + $value = implode(', ', $value); + break; + case 'K': // ...key columns. + // This is like "%LC", but for escaping column lists passed to key + // specifications. These should be escaped as "`column`(123)". For + // example: + // + // ALTER TABLE `x` ADD KEY `y` (`u`(16), `v`(32)); + + foreach ($value as $k => $v) { + $matches = null; + if (preg_match('/\((\d+)\)\z/', $v, $matches)) { + $v = substr($v, 0, -(strlen($matches[1]) + 2)); + $prefix_len = '('.((int)$matches[1]).')'; + } else { + $prefix_len = ''; + } + + $value[$k] = $escaper->escapeColumnName($v).$prefix_len; + } + $value = implode(', ', $value); break; case 'Q': // TODO: Here, and in "%LO", "%LA", and "%LJ", we should eventually // stop accepting strings. foreach ($value as $k => $v) { if (is_string($v)) { continue; } $value[$k] = $v->getUnmaskedString(); } $value = implode(', ', $value); break; case 'O': foreach ($value as $k => $v) { if (is_string($v)) { continue; } $value[$k] = $v->getUnmaskedString(); } if (count($value) == 1) { $value = '('.head($value).')'; } else { $value = '(('.implode(') OR (', $value).'))'; } break; case 'A': foreach ($value as $k => $v) { if (is_string($v)) { continue; } $value[$k] = $v->getUnmaskedString(); } if (count($value) == 1) { $value = '('.head($value).')'; } else { $value = '(('.implode(') AND (', $value).'))'; } break; case 'J': foreach ($value as $k => $v) { if (is_string($v)) { continue; } $value[$k] = $v->getUnmaskedString(); } $value = implode(' ', $value); break; default: throw new XsprintfUnknownConversionException("%L{$next}"); } break; } if (!$done) { qsprintf_check_type($value, $type, $pattern); switch ($type) { case 's': // String if ($nullable && $value === null) { $value = 'NULL'; } else { $value = "'".$escaper->escapeUTF8String((string)$value)."'"; } $type = 's'; break; case 'B': // Binary String if ($nullable && $value === null) { $value = 'NULL'; } else { $value = "'".$escaper->escapeBinaryString((string)$value)."'"; } $type = 's'; break; case 'Q': // Query Fragment if ($value instanceof PhutilQueryString) { $value = $value->getUnmaskedString(); } $type = 's'; break; + case 'Z': // Raw Query Fragment + $type = 's'; + break; + case '~': // Like Substring case '>': // Like Prefix case '<': // Like Suffix $value = $escaper->escapeStringForLikeClause($value); switch ($type) { case '~': $value = "'%".$value."%'"; break; case '>': $value = "'".$value."%'"; break; case '<': $value = "'%".$value."'"; break; } $type = 's'; break; case 'f': // Float if ($nullable && $value === null) { $value = 'NULL'; } else { $value = (float)$value; } $type = 's'; break; case 'd': // Integer if ($nullable && $value === null) { $value = 'NULL'; } else { $value = (int)$value; } $type = 's'; break; case 'T': // Table case 'C': // Column $value = $escaper->escapeColumnName($value); $type = 's'; break; case 'K': // Komment $value = $escaper->escapeMultilineComment($value); $type = 's'; break; case 'R': // Database + Table Reference $database_name = $value->getAphrontRefDatabaseName(); $database_name = $escaper->escapeColumnName($database_name); $table_name = $value->getAphrontRefTableName(); $table_name = $escaper->escapeColumnName($table_name); $value = $database_name.'.'.$table_name; $type = 's'; break; case 'P': // Password or Secret if ($unmasked) { $value = $value->openEnvelope(); $value = "'".$escaper->escapeUTF8String($value)."'"; } else { $value = '********'; } $type = 's'; break; default: throw new XsprintfUnknownConversionException($type); } } if ($prefix) { $value = $prefix.$value; } $pattern[$pos] = $type; } function qsprintf_check_type($value, $type, $query) { switch ($type) { case 'Ld': case 'Ls': case 'LC': + case 'LK': case 'LB': case 'Lf': case 'LQ': case 'LA': case 'LO': case 'LJ': if (!is_array($value)) { throw new AphrontParameterQueryException( $query, pht('Expected array argument for %%%s conversion.', $type)); } if (empty($value)) { throw new AphrontParameterQueryException( $query, pht('Array for %%%s conversion is empty.', $type)); } foreach ($value as $scalar) { qsprintf_check_scalar_type($scalar, $type, $query); } break; default: qsprintf_check_scalar_type($value, $type, $query); break; } } function qsprintf_check_scalar_type($value, $type, $query) { switch ($type) { case 'LQ': case 'LA': case 'LO': case 'LJ': // TODO: See T13217. Remove this eventually. if (is_string($value)) { phlog( pht( 'UNSAFE: Raw string ("%s") passed to query ("%s") subclause '. 'for "%%%s" conversion. Subclause conversions should be passed '. 'a list of PhutilQueryString objects.', $value, $query, $type)); break; } if (!($value instanceof PhutilQueryString)) { throw new AphrontParameterQueryException( $query, pht( 'Expected a list of PhutilQueryString objects for %%%s '. 'conversion.', $type)); } break; case 'Q': // TODO: See T13217. Remove this eventually. if (is_string($value)) { phlog( pht( 'UNSAFE: Raw string ("%s") passed to query ("%s") for "%%Q" '. 'conversion. %%Q should be passed a query string.', $value, $query)); break; } if (!($value instanceof PhutilQueryString)) { throw new AphrontParameterQueryException( $query, pht('Expected a PhutilQueryString for %%%s conversion.', $type)); } break; + case 'Z': + if (!is_string($value)) { + throw new AphrontParameterQueryException( + $query, + pht('Value for "%%Z" conversion should be a raw string.')); + } + break; + case 'LC': + case 'LK': case 'T': case 'C': if (!is_string($value)) { throw new AphrontParameterQueryException( $query, pht('Expected a string for %%%s conversion.', $type)); } break; case 'Ld': case 'Lf': case 'd': case 'f': if (!is_null($value) && !is_numeric($value)) { throw new AphrontParameterQueryException( $query, pht('Expected a numeric scalar or null for %%%s conversion.', $type)); } break; case 'Ls': case 's': case 'LB': case 'B': case '~': case '>': case '<': case 'K': if (!is_null($value) && !is_scalar($value)) { throw new AphrontParameterQueryException( $query, pht('Expected a scalar or null for %%%s conversion.', $type)); } break; case 'R': if (!($value instanceof AphrontDatabaseTableRefInterface)) { throw new AphrontParameterQueryException( pht( 'Parameter to "%s" conversion in "qsprintf(...)" is not an '. 'instance of AphrontDatabaseTableRefInterface.', '%R')); } break; case 'P': if (!($value instanceof PhutilOpaqueEnvelope)) { throw new AphrontParameterQueryException( pht( 'Parameter to "%s" conversion in "qsprintf(...)" is not an '. 'instance of PhutilOpaqueEnvelope.', '%P')); } break; default: throw new XsprintfUnknownConversionException($type); } }