Path: blob/master/src/infrastructure/storage/xsprintf/qsprintf.php
12241 views
<?php12/**3* Format an SQL query. This function behaves like `sprintf`, except that all4* the normal conversions (like "%s") will be properly escaped, and additional5* conversions are supported:6*7* %nd, %ns, %nf, %nB8* "Nullable" versions of %d, %s, %f and %B. Will produce 'NULL' if the9* argument is a strict null.10*11* %=d, %=s, %=f12* "Nullable Test" versions of %d, %s and %f. If you pass a value, you13* get "= 3"; if you pass null, you get "IS NULL". For instance, this14* will work properly if `hatID' is a nullable column and $hat is null.15*16* qsprintf($escaper, 'WHERE hatID %=d', $hat);17*18* %Ld, %Ls, %Lf, %LB19* "List" versions of %d, %s, %f and %B. These are appropriate for use in20* an "IN" clause. For example:21*22* qsprintf($escaper, 'WHERE hatID IN (%Ld)', $list_of_hats);23*24* %B ("Binary String")25* Escapes a string for insertion into a pure binary column, ignoring26* tests for characters outside of the basic multilingual plane.27*28* %C, %LC, %LK ("Column", "Key Column")29* Escapes a column name or a list of column names. The "%LK" variant30* escapes a list of key column specifications which may look like31* "column(32)".32*33* %K ("Comment")34* Escapes a comment.35*36* %Q, %LA, %LO, %LQ, %LJ ("Query Fragment")37* Injects a query fragment from a prior call to qsprintf(). The list38* variants join a list of query fragments with AND, OR, comma, or space.39*40* %Z ("Raw Query")41* Injects a raw, unescaped query fragment. Dangerous!42*43* %R ("Database and Table Reference")44* Behaves like "%T.%T" and prints a full reference to a table including45* the database. Accepts a AphrontDatabaseTableRefInterface.46*47* %P ("Password or Secret")48* Behaves like "%s", but shows "********" when the query is printed in49* logs or traces. Accepts a PhutilOpaqueEnvelope.50*51* %~ ("Substring")52* Escapes a substring query for a LIKE (or NOT LIKE) clause. For example:53*54* // Find all rows with $search as a substring of `name`.55* qsprintf($escaper, 'WHERE name LIKE %~', $search);56*57* See also %> and %<.58*59* %> ("Prefix")60* Escapes a prefix query for a LIKE clause. For example:61*62* // Find all rows where `name` starts with $prefix.63* qsprintf($escaper, 'WHERE name LIKE %>', $prefix);64*65* %< ("Suffix")66* Escapes a suffix query for a LIKE clause. For example:67*68* // Find all rows where `name` ends with $suffix.69* qsprintf($escaper, 'WHERE name LIKE %<', $suffix);70*71* %T ("Table")72* Escapes a table name. In most cases, you should use "%R" instead.73*/74function qsprintf(PhutilQsprintfInterface $escaper, $pattern /* , ... */) {75$args = func_get_args();76array_shift($args);77return new PhutilQueryString($escaper, $args);78}7980function vqsprintf(PhutilQsprintfInterface $escaper, $pattern, array $argv) {81array_unshift($argv, $pattern);82return new PhutilQueryString($escaper, $argv);83}8485/**86* @{function:xsprintf} callback for encoding SQL queries. See87* @{function:qsprintf}.88*/89function xsprintf_query($userdata, &$pattern, &$pos, &$value, &$length) {90$type = $pattern[$pos];9192if (is_array($userdata)) {93$escaper = $userdata['escaper'];94$unmasked = $userdata['unmasked'];95} else {96$escaper = $userdata;97$unmasked = false;98}99100$next = (strlen($pattern) > $pos + 1) ? $pattern[$pos + 1] : null;101$nullable = false;102$done = false;103104$prefix = '';105106if (!($escaper instanceof PhutilQsprintfInterface)) {107throw new InvalidArgumentException(pht('Invalid database escaper.'));108}109110switch ($type) {111case '=': // Nullable test112switch ($next) {113case 'd':114case 'f':115case 's':116$pattern = substr_replace($pattern, '', $pos, 1);117$length = strlen($pattern);118$type = 's';119if ($value === null) {120$value = 'IS NULL';121$done = true;122} else {123$prefix = '= ';124$type = $next;125}126break;127default:128throw new Exception(129pht(130'Unknown conversion, try %s, %s, or %s.',131'%=d',132'%=s',133'%=f'));134}135break;136137case 'n': // Nullable...138switch ($next) {139case 'd': // ...integer.140case 'f': // ...float.141case 's': // ...string.142case 'B': // ...binary string.143$pattern = substr_replace($pattern, '', $pos, 1);144$length = strlen($pattern);145$type = $next;146$nullable = true;147break;148default:149throw new XsprintfUnknownConversionException("%n{$next}");150}151break;152153case 'L': // List of..154qsprintf_check_type($value, "L{$next}", $pattern);155$pattern = substr_replace($pattern, '', $pos, 1);156$length = strlen($pattern);157$type = 's';158$done = true;159160switch ($next) {161case 'd': // ...integers.162$value = implode(', ', array_map('intval', $value));163break;164case 'f': // ...floats.165$value = implode(', ', array_map('floatval', $value));166break;167case 's': // ...strings.168foreach ($value as $k => $v) {169$value[$k] = "'".$escaper->escapeUTF8String((string)$v)."'";170}171$value = implode(', ', $value);172break;173case 'B': // ...binary strings.174foreach ($value as $k => $v) {175$value[$k] = "'".$escaper->escapeBinaryString((string)$v)."'";176}177$value = implode(', ', $value);178break;179case 'C': // ...columns.180foreach ($value as $k => $v) {181$value[$k] = $escaper->escapeColumnName($v);182}183$value = implode(', ', $value);184break;185case 'K': // ...key columns.186// This is like "%LC", but for escaping column lists passed to key187// specifications. These should be escaped as "`column`(123)". For188// example:189//190// ALTER TABLE `x` ADD KEY `y` (`u`(16), `v`(32));191192foreach ($value as $k => $v) {193$matches = null;194if (preg_match('/\((\d+)\)\z/', $v, $matches)) {195$v = substr($v, 0, -(strlen($matches[1]) + 2));196$prefix_len = '('.((int)$matches[1]).')';197} else {198$prefix_len = '';199}200201$value[$k] = $escaper->escapeColumnName($v).$prefix_len;202}203204$value = implode(', ', $value);205break;206case 'Q':207// TODO: Here, and in "%LO", "%LA", and "%LJ", we should eventually208// stop accepting strings.209foreach ($value as $k => $v) {210if (is_string($v)) {211continue;212}213$value[$k] = $v->getUnmaskedString();214}215$value = implode(', ', $value);216break;217case 'O':218foreach ($value as $k => $v) {219if (is_string($v)) {220continue;221}222$value[$k] = $v->getUnmaskedString();223}224if (count($value) == 1) {225$value = '('.head($value).')';226} else {227$value = '(('.implode(') OR (', $value).'))';228}229break;230case 'A':231foreach ($value as $k => $v) {232if (is_string($v)) {233continue;234}235$value[$k] = $v->getUnmaskedString();236}237if (count($value) == 1) {238$value = '('.head($value).')';239} else {240$value = '(('.implode(') AND (', $value).'))';241}242break;243case 'J':244foreach ($value as $k => $v) {245if (is_string($v)) {246continue;247}248$value[$k] = $v->getUnmaskedString();249}250$value = implode(' ', $value);251break;252default:253throw new XsprintfUnknownConversionException("%L{$next}");254}255break;256}257258if (!$done) {259qsprintf_check_type($value, $type, $pattern);260switch ($type) {261case 's': // String262if ($nullable && $value === null) {263$value = 'NULL';264} else {265$value = "'".$escaper->escapeUTF8String((string)$value)."'";266}267$type = 's';268break;269270case 'B': // Binary String271if ($nullable && $value === null) {272$value = 'NULL';273} else {274$value = "'".$escaper->escapeBinaryString((string)$value)."'";275}276$type = 's';277break;278279case 'Q': // Query Fragment280if ($value instanceof PhutilQueryString) {281$value = $value->getUnmaskedString();282}283$type = 's';284break;285286case 'Z': // Raw Query Fragment287$type = 's';288break;289290case '~': // Like Substring291case '>': // Like Prefix292case '<': // Like Suffix293$value = $escaper->escapeStringForLikeClause($value);294switch ($type) {295case '~': $value = "'%".$value."%'"; break;296case '>': $value = "'".$value."%'"; break;297case '<': $value = "'%".$value."'"; break;298}299$type = 's';300break;301302case 'f': // Float303if ($nullable && $value === null) {304$value = 'NULL';305} else {306$value = (float)$value;307}308$type = 's';309break;310311case 'd': // Integer312if ($nullable && $value === null) {313$value = 'NULL';314} else {315$value = (int)$value;316}317$type = 's';318break;319320case 'T': // Table321case 'C': // Column322$value = $escaper->escapeColumnName($value);323$type = 's';324break;325326case 'K': // Komment327$value = $escaper->escapeMultilineComment($value);328$type = 's';329break;330331case 'R': // Database + Table Reference332$database_name = $value->getAphrontRefDatabaseName();333$database_name = $escaper->escapeColumnName($database_name);334335$table_name = $value->getAphrontRefTableName();336$table_name = $escaper->escapeColumnName($table_name);337338$value = $database_name.'.'.$table_name;339$type = 's';340break;341342case 'P': // Password or Secret343if ($unmasked) {344$value = $value->openEnvelope();345$value = "'".$escaper->escapeUTF8String($value)."'";346} else {347$value = '********';348}349$type = 's';350break;351352default:353throw new XsprintfUnknownConversionException($type);354}355}356357if ($prefix) {358$value = $prefix.$value;359}360361$pattern[$pos] = $type;362}363364function qsprintf_check_type($value, $type, $query) {365switch ($type) {366case 'Ld':367case 'Ls':368case 'LC':369case 'LK':370case 'LB':371case 'Lf':372case 'LQ':373case 'LA':374case 'LO':375case 'LJ':376if (!is_array($value)) {377throw new AphrontParameterQueryException(378$query,379pht('Expected array argument for %%%s conversion.', $type));380}381if (empty($value)) {382throw new AphrontParameterQueryException(383$query,384pht('Array for %%%s conversion is empty.', $type));385}386387foreach ($value as $scalar) {388qsprintf_check_scalar_type($scalar, $type, $query);389}390break;391default:392qsprintf_check_scalar_type($value, $type, $query);393break;394}395}396397function qsprintf_check_scalar_type($value, $type, $query) {398switch ($type) {399case 'LQ':400case 'LA':401case 'LO':402case 'LJ':403// TODO: See T13217. Remove this eventually.404if (is_string($value)) {405phlog(406pht(407'UNSAFE: Raw string ("%s") passed to query ("%s") subclause '.408'for "%%%s" conversion. Subclause conversions should be passed '.409'a list of PhutilQueryString objects.',410$value,411$query,412$type));413break;414}415416if (!($value instanceof PhutilQueryString)) {417throw new AphrontParameterQueryException(418$query,419pht(420'Expected a list of PhutilQueryString objects for %%%s '.421'conversion.',422$type));423}424break;425426case 'Q':427// TODO: See T13217. Remove this eventually.428if (is_string($value)) {429phlog(430pht(431'UNSAFE: Raw string ("%s") passed to query ("%s") for "%%Q" '.432'conversion. %%Q should be passed a query string.',433$value,434$query));435break;436}437438if (!($value instanceof PhutilQueryString)) {439throw new AphrontParameterQueryException(440$query,441pht('Expected a PhutilQueryString for %%%s conversion.', $type));442}443break;444445case 'Z':446if (!is_string($value)) {447throw new AphrontParameterQueryException(448$query,449pht('Value for "%%Z" conversion should be a raw string.'));450}451break;452453case 'LC':454case 'LK':455case 'T':456case 'C':457if (!is_string($value)) {458throw new AphrontParameterQueryException(459$query,460pht('Expected a string for %%%s conversion.', $type));461}462break;463464case 'Ld':465case 'Lf':466case 'd':467case 'f':468if (!is_null($value) && !is_numeric($value)) {469throw new AphrontParameterQueryException(470$query,471pht('Expected a numeric scalar or null for %%%s conversion.', $type));472}473break;474475case 'Ls':476case 's':477case 'LB':478case 'B':479case '~':480case '>':481case '<':482case 'K':483if (!is_null($value) && !is_scalar($value)) {484throw new AphrontParameterQueryException(485$query,486pht('Expected a scalar or null for %%%s conversion.', $type));487}488break;489490case 'R':491if (!($value instanceof AphrontDatabaseTableRefInterface)) {492throw new AphrontParameterQueryException(493$query,494pht(495'Parameter to "%s" conversion in "qsprintf(...)" is not an '.496'instance of AphrontDatabaseTableRefInterface.',497'%R'));498}499break;500501case 'P':502if (!($value instanceof PhutilOpaqueEnvelope)) {503throw new AphrontParameterQueryException(504$query,505pht(506'Parameter to "%s" conversion in "qsprintf(...)" is not an '.507'instance of PhutilOpaqueEnvelope.',508'%P'));509}510break;511512default:513throw new XsprintfUnknownConversionException($type);514}515}516517518