4 * Format an SQL query. This function behaves like `sprintf`, except that all
5 * the normal conversions (like "%s") will be properly escaped, and additional
6 * conversions are supported:
9 * "Nullable" versions of %d, %s, %f and %B. Will produce 'NULL' if the
10 * argument is a strict null.
13 * "Nullable Test" versions of %d, %s and %f. If you pass a value, you
14 * get "= 3"; if you pass null, you get "IS NULL". For instance, this
15 * will work properly if `hatID' is a nullable column and $hat is null.
17 * qsprintf($escaper, 'WHERE hatID %=d', $hat);
20 * "List" versions of %d, %s, %f and %B. These are appropriate for use in
21 * an "IN" clause. For example:
23 * qsprintf($escaper, 'WHERE hatID IN (%Ld)', $list_of_hats);
25 * %B ("Binary String")
26 * Escapes a string for insertion into a pure binary column, ignoring
27 * tests for characters outside of the basic multilingual plane.
29 * %C, %LC, %LK ("Column", "Key Column")
30 * Escapes a column name or a list of column names. The "%LK" variant
31 * escapes a list of key column specifications which may look like
37 * %Q, %LA, %LO, %LQ, %LJ ("Query Fragment")
38 * Injects a query fragment from a prior call to qsprintf(). The list
39 * variants join a list of query fragments with AND, OR, comma, or space.
42 * Injects a raw, unescaped query fragment. Dangerous!
44 * %R ("Database and Table Reference")
45 * Behaves like "%T.%T" and prints a full reference to a table including
46 * the database. Accepts a AphrontDatabaseTableRefInterface.
48 * %P ("Password or Secret")
49 * Behaves like "%s", but shows "********" when the query is printed in
50 * logs or traces. Accepts a PhutilOpaqueEnvelope.
53 * Escapes a substring query for a LIKE (or NOT LIKE) clause. For example:
55 * // Find all rows with $search as a substring of `name`.
56 * qsprintf($escaper, 'WHERE name LIKE %~', $search);
61 * Escapes a prefix query for a LIKE clause. For example:
63 * // Find all rows where `name` starts with $prefix.
64 * qsprintf($escaper, 'WHERE name LIKE %>', $prefix);
67 * Escapes a suffix query for a LIKE clause. For example:
69 * // Find all rows where `name` ends with $suffix.
70 * qsprintf($escaper, 'WHERE name LIKE %<', $suffix);
73 * Escapes a table name. In most cases, you should use "%R" instead.
75 function qsprintf(PhutilQsprintfInterface
$escaper, $pattern /* , ... */) {
76 $args = func_get_args();
78 return new PhutilQueryString($escaper, $args);
81 function vqsprintf(PhutilQsprintfInterface
$escaper, $pattern, array $argv) {
82 array_unshift($argv, $pattern);
83 return new PhutilQueryString($escaper, $argv);
87 * @{function:xsprintf} callback for encoding SQL queries. See
88 * @{function:qsprintf}.
90 function xsprintf_query($userdata, &$pattern, &$pos, &$value, &$length) {
91 $type = $pattern[$pos];
93 if (is_array($userdata)) {
94 $escaper = $userdata['escaper'];
95 $unmasked = $userdata['unmasked'];
101 $next = (strlen($pattern) > $pos +
1) ?
$pattern[$pos +
1] : null;
107 if (!($escaper instanceof PhutilQsprintfInterface
)) {
108 throw new InvalidArgumentException(pht('Invalid database escaper.'));
112 case '=': // Nullable test
117 $pattern = substr_replace($pattern, '', $pos, 1);
118 $length = strlen($pattern);
120 if ($value === null) {
131 'Unknown conversion, try %s, %s, or %s.',
138 case 'n': // Nullable...
140 case 'd': // ...integer.
141 case 'f': // ...float.
142 case 's': // ...string.
143 case 'B': // ...binary string.
144 $pattern = substr_replace($pattern, '', $pos, 1);
145 $length = strlen($pattern);
150 throw new XsprintfUnknownConversionException("%n{$next}");
154 case 'L': // List of..
155 qsprintf_check_type($value, "L{$next}", $pattern);
156 $pattern = substr_replace($pattern, '', $pos, 1);
157 $length = strlen($pattern);
162 case 'd': // ...integers.
163 $value = implode(', ', array_map('intval', $value));
165 case 'f': // ...floats.
166 $value = implode(', ', array_map('floatval', $value));
168 case 's': // ...strings.
169 foreach ($value as $k => $v) {
170 $value[$k] = "'".$escaper->escapeUTF8String((string)$v)."'";
172 $value = implode(', ', $value);
174 case 'B': // ...binary strings.
175 foreach ($value as $k => $v) {
176 $value[$k] = "'".$escaper->escapeBinaryString((string)$v)."'";
178 $value = implode(', ', $value);
180 case 'C': // ...columns.
181 foreach ($value as $k => $v) {
182 $value[$k] = $escaper->escapeColumnName($v);
184 $value = implode(', ', $value);
186 case 'K': // ...key columns.
187 // This is like "%LC", but for escaping column lists passed to key
188 // specifications. These should be escaped as "`column`(123)". For
191 // ALTER TABLE `x` ADD KEY `y` (`u`(16), `v`(32));
193 foreach ($value as $k => $v) {
195 if (preg_match('/\((\d+)\)\z/', $v, $matches)) {
196 $v = substr($v, 0, -(strlen($matches[1]) +
2));
197 $prefix_len = '('.((int)$matches[1]).')';
202 $value[$k] = $escaper->escapeColumnName($v).$prefix_len;
205 $value = implode(', ', $value);
208 // TODO: Here, and in "%LO", "%LA", and "%LJ", we should eventually
209 // stop accepting strings.
210 foreach ($value as $k => $v) {
214 $value[$k] = $v->getUnmaskedString();
216 $value = implode(', ', $value);
219 foreach ($value as $k => $v) {
223 $value[$k] = $v->getUnmaskedString();
225 if (count($value) == 1) {
226 $value = '('.head($value).')';
228 $value = '(('.implode(') OR (', $value).'))';
232 foreach ($value as $k => $v) {
236 $value[$k] = $v->getUnmaskedString();
238 if (count($value) == 1) {
239 $value = '('.head($value).')';
241 $value = '(('.implode(') AND (', $value).'))';
245 foreach ($value as $k => $v) {
249 $value[$k] = $v->getUnmaskedString();
251 $value = implode(' ', $value);
254 throw new XsprintfUnknownConversionException("%L{$next}");
260 qsprintf_check_type($value, $type, $pattern);
263 if ($nullable && $value === null) {
266 $value = "'".$escaper->escapeUTF8String((string)$value)."'";
271 case 'B': // Binary String
272 if ($nullable && $value === null) {
275 $value = "'".$escaper->escapeBinaryString((string)$value)."'";
280 case 'Q': // Query Fragment
281 if ($value instanceof PhutilQueryString
) {
282 $value = $value->getUnmaskedString();
287 case 'Z': // Raw Query Fragment
291 case '~': // Like Substring
292 case '>': // Like Prefix
293 case '<': // Like Suffix
294 $value = $escaper->escapeStringForLikeClause($value);
296 case '~': $value = "'%".$value."%'"; break;
297 case '>': $value = "'".$value."%'"; break;
298 case '<': $value = "'%".$value."'"; break;
304 if ($nullable && $value === null) {
307 $value = (float)$value;
313 if ($nullable && $value === null) {
316 $value = (int)$value;
323 $value = $escaper->escapeColumnName($value);
328 $value = $escaper->escapeMultilineComment($value);
332 case 'R': // Database + Table Reference
333 $database_name = $value->getAphrontRefDatabaseName();
334 $database_name = $escaper->escapeColumnName($database_name);
336 $table_name = $value->getAphrontRefTableName();
337 $table_name = $escaper->escapeColumnName($table_name);
339 $value = $database_name.'.'.$table_name;
343 case 'P': // Password or Secret
345 $value = $value->openEnvelope();
346 $value = "'".$escaper->escapeUTF8String($value)."'";
354 throw new XsprintfUnknownConversionException($type);
359 $value = $prefix.$value;
362 $pattern[$pos] = $type;
365 function qsprintf_check_type($value, $type, $query) {
377 if (!is_array($value)) {
378 throw new AphrontParameterQueryException(
380 pht('Expected array argument for %%%s conversion.', $type));
383 throw new AphrontParameterQueryException(
385 pht('Array for %%%s conversion is empty.', $type));
388 foreach ($value as $scalar) {
389 qsprintf_check_scalar_type($scalar, $type, $query);
393 qsprintf_check_scalar_type($value, $type, $query);
398 function qsprintf_check_scalar_type($value, $type, $query) {
404 // TODO: See T13217. Remove this eventually.
405 if (is_string($value)) {
408 'UNSAFE: Raw string ("%s") passed to query ("%s") subclause '.
409 'for "%%%s" conversion. Subclause conversions should be passed '.
410 'a list of PhutilQueryString objects.',
417 if (!($value instanceof PhutilQueryString
)) {
418 throw new AphrontParameterQueryException(
421 'Expected a list of PhutilQueryString objects for %%%s '.
428 // TODO: See T13217. Remove this eventually.
429 if (is_string($value)) {
432 'UNSAFE: Raw string ("%s") passed to query ("%s") for "%%Q" '.
433 'conversion. %%Q should be passed a query string.',
439 if (!($value instanceof PhutilQueryString
)) {
440 throw new AphrontParameterQueryException(
442 pht('Expected a PhutilQueryString for %%%s conversion.', $type));
447 if (!is_string($value)) {
448 throw new AphrontParameterQueryException(
450 pht('Value for "%%Z" conversion should be a raw string.'));
458 if (!is_string($value)) {
459 throw new AphrontParameterQueryException(
461 pht('Expected a string for %%%s conversion.', $type));
469 if (!is_null($value) && !is_numeric($value)) {
470 throw new AphrontParameterQueryException(
472 pht('Expected a numeric scalar or null for %%%s conversion.', $type));
484 if (!is_null($value) && !is_scalar($value)) {
485 throw new AphrontParameterQueryException(
487 pht('Expected a scalar or null for %%%s conversion.', $type));
492 if (!($value instanceof AphrontDatabaseTableRefInterface
)) {
493 throw new AphrontParameterQueryException(
496 'Parameter to "%s" conversion in "qsprintf(...)" is not an '.
497 'instance of AphrontDatabaseTableRefInterface.',
503 if (!($value instanceof PhutilOpaqueEnvelope
)) {
504 throw new AphrontParameterQueryException(
507 'Parameter to "%s" conversion in "qsprintf(...)" is not an '.
508 'instance of PhutilOpaqueEnvelope.',
514 throw new XsprintfUnknownConversionException($type);