1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.database.sqlite; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.ContentValues; 22 import android.database.Cursor; 23 import android.database.DatabaseUtils; 24 import android.os.Build; 25 import android.os.CancellationSignal; 26 import android.os.OperationCanceledException; 27 import android.provider.BaseColumns; 28 import android.text.TextUtils; 29 import android.util.Log; 30 31 import com.android.internal.util.ArrayUtils; 32 33 import libcore.util.EmptyArray; 34 35 import java.util.Arrays; 36 import java.util.Iterator; 37 import java.util.List; 38 import java.util.Locale; 39 import java.util.Map; 40 import java.util.Map.Entry; 41 import java.util.Objects; 42 import java.util.Set; 43 import java.util.regex.Matcher; 44 import java.util.regex.Pattern; 45 46 /** 47 * This is a convience class that helps build SQL queries to be sent to 48 * {@link SQLiteDatabase} objects. 49 */ 50 public class SQLiteQueryBuilder 51 { 52 private static final String TAG = "SQLiteQueryBuilder"; 53 54 private Map<String, String> mProjectionMap = null; 55 56 private static final Pattern sAggregationPattern = Pattern.compile( 57 "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT)\\((.+)\\)"); 58 59 private List<Pattern> mProjectionGreylist = null; 60 61 private String mTables = ""; 62 private StringBuilder mWhereClause = null; // lazily created 63 private boolean mDistinct; 64 private SQLiteDatabase.CursorFactory mFactory; 65 66 private static final int STRICT_PARENTHESES = 1 << 0; 67 private static final int STRICT_COLUMNS = 1 << 1; 68 private static final int STRICT_GRAMMAR = 1 << 2; 69 70 private int mStrictFlags; 71 SQLiteQueryBuilder()72 public SQLiteQueryBuilder() { 73 mDistinct = false; 74 mFactory = null; 75 } 76 77 /** 78 * Mark the query as DISTINCT. 79 * 80 * @param distinct if true the query is DISTINCT, otherwise it isn't 81 */ setDistinct(boolean distinct)82 public void setDistinct(boolean distinct) { 83 mDistinct = distinct; 84 } 85 86 /** 87 * Returns the list of tables being queried 88 * 89 * @return the list of tables being queried 90 */ getTables()91 public String getTables() { 92 return mTables; 93 } 94 95 /** 96 * Sets the list of tables to query. Multiple tables can be specified to perform a join. 97 * For example: 98 * setTables("foo, bar") 99 * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)") 100 * 101 * @param inTables the list of tables to query on 102 */ setTables(String inTables)103 public void setTables(String inTables) { 104 mTables = inTables; 105 } 106 107 /** 108 * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded 109 * by parenthesis and ANDed with the selection passed to {@link #query}. The final 110 * WHERE clause looks like: 111 * 112 * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) 113 * 114 * @param inWhere the chunk of text to append to the WHERE clause. 115 */ appendWhere(CharSequence inWhere)116 public void appendWhere(CharSequence inWhere) { 117 if (mWhereClause == null) { 118 mWhereClause = new StringBuilder(inWhere.length() + 16); 119 } 120 mWhereClause.append(inWhere); 121 } 122 123 /** 124 * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded 125 * by parenthesis and ANDed with the selection passed to {@link #query}. The final 126 * WHERE clause looks like: 127 * 128 * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) 129 * 130 * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped 131 * to avoid SQL injection attacks 132 */ appendWhereEscapeString(String inWhere)133 public void appendWhereEscapeString(String inWhere) { 134 if (mWhereClause == null) { 135 mWhereClause = new StringBuilder(inWhere.length() + 16); 136 } 137 DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere); 138 } 139 140 /** 141 * Sets the projection map for the query. The projection map maps 142 * from column names that the caller passes into query to database 143 * column names. This is useful for renaming columns as well as 144 * disambiguating column names when doing joins. For example you 145 * could map "name" to "people.name". If a projection map is set 146 * it must contain all column names the user may request, even if 147 * the key and value are the same. 148 * 149 * @param columnMap maps from the user column names to the database column names 150 */ setProjectionMap(Map<String, String> columnMap)151 public void setProjectionMap(Map<String, String> columnMap) { 152 mProjectionMap = columnMap; 153 } 154 155 /** 156 * Gets the projection map for the query, as last configured by 157 * {@link #setProjectionMap(Map)}. 158 * 159 * @hide 160 */ getProjectionMap()161 public @Nullable Map<String, String> getProjectionMap() { 162 return mProjectionMap; 163 } 164 165 /** 166 * Sets a projection greylist of columns that will be allowed through, even 167 * when {@link #setStrict(boolean)} is enabled. This provides a way for 168 * abusive custom columns like {@code COUNT(*)} to continue working. 169 * 170 * @hide 171 */ setProjectionGreylist(@ullable List<Pattern> projectionGreylist)172 public void setProjectionGreylist(@Nullable List<Pattern> projectionGreylist) { 173 mProjectionGreylist = projectionGreylist; 174 } 175 176 /** 177 * Gets the projection greylist for the query, as last configured by 178 * {@link #setProjectionGreylist(List)}. 179 * 180 * @hide 181 */ getProjectionGreylist()182 public @Nullable List<Pattern> getProjectionGreylist() { 183 return mProjectionGreylist; 184 } 185 186 /** 187 * Sets the cursor factory to be used for the query. You can use 188 * one factory for all queries on a database but it is normally 189 * easier to specify the factory when doing this query. 190 * 191 * @param factory the factory to use. 192 */ setCursorFactory(SQLiteDatabase.CursorFactory factory)193 public void setCursorFactory(SQLiteDatabase.CursorFactory factory) { 194 mFactory = factory; 195 } 196 197 /** 198 * When set, the selection is verified against malicious arguments. 199 * When using this class to create a statement using 200 * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)}, 201 * non-numeric limits will raise an exception. If a projection map is specified, fields 202 * not in that map will be ignored. 203 * If this class is used to execute the statement directly using 204 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)} 205 * or 206 * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)}, 207 * additionally also parenthesis escaping selection are caught. 208 * 209 * To summarize: To get maximum protection against malicious third party apps (for example 210 * content provider consumers), make sure to do the following: 211 * <ul> 212 * <li>Set this value to true</li> 213 * <li>Use a projection map</li> 214 * <li>Use one of the query overloads instead of getting the statement as a sql string</li> 215 * </ul> 216 * By default, this value is false. 217 */ setStrict(boolean strict)218 public void setStrict(boolean strict) { 219 if (strict) { 220 mStrictFlags |= STRICT_PARENTHESES; 221 } else { 222 mStrictFlags &= ~STRICT_PARENTHESES; 223 } 224 } 225 226 /** 227 * Get if the query is marked as strict, as last configured by 228 * {@link #setStrict(boolean)}. 229 * 230 * @hide 231 */ isStrict()232 public boolean isStrict() { 233 return (mStrictFlags & STRICT_PARENTHESES) != 0; 234 } 235 236 /** 237 * When enabled, verify that all projections and {@link ContentValues} only 238 * contain valid columns as defined by {@link #setProjectionMap(Map)}. 239 * <p> 240 * This enforcement applies to {@link #insert}, {@link #query}, and 241 * {@link #update} operations. Any enforcement failures will throw an 242 * {@link IllegalArgumentException}. 243 * 244 * @hide 245 */ setStrictColumns(boolean strictColumns)246 public void setStrictColumns(boolean strictColumns) { 247 if (strictColumns) { 248 mStrictFlags |= STRICT_COLUMNS; 249 } else { 250 mStrictFlags &= ~STRICT_COLUMNS; 251 } 252 } 253 254 /** 255 * Get if the query is marked as strict, as last configured by 256 * {@link #setStrictColumns(boolean)}. 257 * 258 * @hide 259 */ isStrictColumns()260 public boolean isStrictColumns() { 261 return (mStrictFlags & STRICT_COLUMNS) != 0; 262 } 263 264 /** 265 * When enabled, verify that all untrusted SQL conforms to a restricted SQL 266 * grammar. Here are the restrictions applied: 267 * <ul> 268 * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and 269 * windowing terms are rejected. 270 * <li>In {@code GROUP BY} clauses: only valid columns are allowed. 271 * <li>In {@code ORDER BY} clauses: only valid columns, collation, and 272 * ordering terms are allowed. 273 * <li>In {@code LIMIT} clauses: only numerical values and offset terms are 274 * allowed. 275 * </ul> 276 * All column references must be valid as defined by 277 * {@link #setProjectionMap(Map)}. 278 * <p> 279 * This enforcement applies to {@link #query}, {@link #update} and 280 * {@link #delete} operations. This enforcement does not apply to trusted 281 * inputs, such as those provided by {@link #appendWhere}. Any enforcement 282 * failures will throw an {@link IllegalArgumentException}. 283 * 284 * @hide 285 */ setStrictGrammar(boolean strictGrammar)286 public void setStrictGrammar(boolean strictGrammar) { 287 if (strictGrammar) { 288 mStrictFlags |= STRICT_GRAMMAR; 289 } else { 290 mStrictFlags &= ~STRICT_GRAMMAR; 291 } 292 } 293 294 /** 295 * Get if the query is marked as strict, as last configured by 296 * {@link #setStrictGrammar(boolean)}. 297 * 298 * @hide 299 */ isStrictGrammar()300 public boolean isStrictGrammar() { 301 return (mStrictFlags & STRICT_GRAMMAR) != 0; 302 } 303 304 /** 305 * Build an SQL query string from the given clauses. 306 * 307 * @param distinct true if you want each row to be unique, false otherwise. 308 * @param tables The table names to compile the query against. 309 * @param columns A list of which columns to return. Passing null will 310 * return all columns, which is discouraged to prevent reading 311 * data from storage that isn't going to be used. 312 * @param where A filter declaring which rows to return, formatted as an SQL 313 * WHERE clause (excluding the WHERE itself). Passing null will 314 * return all rows for the given URL. 315 * @param groupBy A filter declaring how to group rows, formatted as an SQL 316 * GROUP BY clause (excluding the GROUP BY itself). Passing null 317 * will cause the rows to not be grouped. 318 * @param having A filter declare which row groups to include in the cursor, 319 * if row grouping is being used, formatted as an SQL HAVING 320 * clause (excluding the HAVING itself). Passing null will cause 321 * all row groups to be included, and is required when row 322 * grouping is not being used. 323 * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause 324 * (excluding the ORDER BY itself). Passing null will use the 325 * default sort order, which may be unordered. 326 * @param limit Limits the number of rows returned by the query, 327 * formatted as LIMIT clause. Passing null denotes no LIMIT clause. 328 * @return the SQL query string 329 */ buildQueryString( boolean distinct, String tables, String[] columns, String where, String groupBy, String having, String orderBy, String limit)330 public static String buildQueryString( 331 boolean distinct, String tables, String[] columns, String where, 332 String groupBy, String having, String orderBy, String limit) { 333 if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) { 334 throw new IllegalArgumentException( 335 "HAVING clauses are only permitted when using a groupBy clause"); 336 } 337 338 StringBuilder query = new StringBuilder(120); 339 340 query.append("SELECT "); 341 if (distinct) { 342 query.append("DISTINCT "); 343 } 344 if (columns != null && columns.length != 0) { 345 appendColumns(query, columns); 346 } else { 347 query.append("* "); 348 } 349 query.append("FROM "); 350 query.append(tables); 351 appendClause(query, " WHERE ", where); 352 appendClause(query, " GROUP BY ", groupBy); 353 appendClause(query, " HAVING ", having); 354 appendClause(query, " ORDER BY ", orderBy); 355 appendClause(query, " LIMIT ", limit); 356 357 return query.toString(); 358 } 359 appendClause(StringBuilder s, String name, String clause)360 private static void appendClause(StringBuilder s, String name, String clause) { 361 if (!TextUtils.isEmpty(clause)) { 362 s.append(name); 363 s.append(clause); 364 } 365 } 366 367 /** 368 * Add the names that are non-null in columns to s, separating 369 * them with commas. 370 */ appendColumns(StringBuilder s, String[] columns)371 public static void appendColumns(StringBuilder s, String[] columns) { 372 int n = columns.length; 373 374 for (int i = 0; i < n; i++) { 375 String column = columns[i]; 376 377 if (column != null) { 378 if (i > 0) { 379 s.append(", "); 380 } 381 s.append(column); 382 } 383 } 384 s.append(' '); 385 } 386 387 /** 388 * Perform a query by combining all current settings and the 389 * information passed into this method. 390 * 391 * @param db the database to query on 392 * @param projectionIn A list of which columns to return. Passing 393 * null will return all columns, which is discouraged to prevent 394 * reading data from storage that isn't going to be used. 395 * @param selection A filter declaring which rows to return, 396 * formatted as an SQL WHERE clause (excluding the WHERE 397 * itself). Passing null will return all rows for the given URL. 398 * @param selectionArgs You may include ?s in selection, which 399 * will be replaced by the values from selectionArgs, in order 400 * that they appear in the selection. The values will be bound 401 * as Strings. 402 * @param groupBy A filter declaring how to group rows, formatted 403 * as an SQL GROUP BY clause (excluding the GROUP BY 404 * itself). Passing null will cause the rows to not be grouped. 405 * @param having A filter declare which row groups to include in 406 * the cursor, if row grouping is being used, formatted as an 407 * SQL HAVING clause (excluding the HAVING itself). Passing 408 * null will cause all row groups to be included, and is 409 * required when row grouping is not being used. 410 * @param sortOrder How to order the rows, formatted as an SQL 411 * ORDER BY clause (excluding the ORDER BY itself). Passing null 412 * will use the default sort order, which may be unordered. 413 * @return a cursor over the result set 414 * @see android.content.ContentResolver#query(android.net.Uri, String[], 415 * String, String[], String) 416 */ query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder)417 public Cursor query(SQLiteDatabase db, String[] projectionIn, 418 String selection, String[] selectionArgs, String groupBy, 419 String having, String sortOrder) { 420 return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder, 421 null /* limit */, null /* cancellationSignal */); 422 } 423 424 /** 425 * Perform a query by combining all current settings and the 426 * information passed into this method. 427 * 428 * @param db the database to query on 429 * @param projectionIn A list of which columns to return. Passing 430 * null will return all columns, which is discouraged to prevent 431 * reading data from storage that isn't going to be used. 432 * @param selection A filter declaring which rows to return, 433 * formatted as an SQL WHERE clause (excluding the WHERE 434 * itself). Passing null will return all rows for the given URL. 435 * @param selectionArgs You may include ?s in selection, which 436 * will be replaced by the values from selectionArgs, in order 437 * that they appear in the selection. The values will be bound 438 * as Strings. 439 * @param groupBy A filter declaring how to group rows, formatted 440 * as an SQL GROUP BY clause (excluding the GROUP BY 441 * itself). Passing null will cause the rows to not be grouped. 442 * @param having A filter declare which row groups to include in 443 * the cursor, if row grouping is being used, formatted as an 444 * SQL HAVING clause (excluding the HAVING itself). Passing 445 * null will cause all row groups to be included, and is 446 * required when row grouping is not being used. 447 * @param sortOrder How to order the rows, formatted as an SQL 448 * ORDER BY clause (excluding the ORDER BY itself). Passing null 449 * will use the default sort order, which may be unordered. 450 * @param limit Limits the number of rows returned by the query, 451 * formatted as LIMIT clause. Passing null denotes no LIMIT clause. 452 * @return a cursor over the result set 453 * @see android.content.ContentResolver#query(android.net.Uri, String[], 454 * String, String[], String) 455 */ query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit)456 public Cursor query(SQLiteDatabase db, String[] projectionIn, 457 String selection, String[] selectionArgs, String groupBy, 458 String having, String sortOrder, String limit) { 459 return query(db, projectionIn, selection, selectionArgs, 460 groupBy, having, sortOrder, limit, null); 461 } 462 463 /** 464 * Perform a query by combining all current settings and the 465 * information passed into this method. 466 * 467 * @param db the database to query on 468 * @param projectionIn A list of which columns to return. Passing 469 * null will return all columns, which is discouraged to prevent 470 * reading data from storage that isn't going to be used. 471 * @param selection A filter declaring which rows to return, 472 * formatted as an SQL WHERE clause (excluding the WHERE 473 * itself). Passing null will return all rows for the given URL. 474 * @param selectionArgs You may include ?s in selection, which 475 * will be replaced by the values from selectionArgs, in order 476 * that they appear in the selection. The values will be bound 477 * as Strings. 478 * @param groupBy A filter declaring how to group rows, formatted 479 * as an SQL GROUP BY clause (excluding the GROUP BY 480 * itself). Passing null will cause the rows to not be grouped. 481 * @param having A filter declare which row groups to include in 482 * the cursor, if row grouping is being used, formatted as an 483 * SQL HAVING clause (excluding the HAVING itself). Passing 484 * null will cause all row groups to be included, and is 485 * required when row grouping is not being used. 486 * @param sortOrder How to order the rows, formatted as an SQL 487 * ORDER BY clause (excluding the ORDER BY itself). Passing null 488 * will use the default sort order, which may be unordered. 489 * @param limit Limits the number of rows returned by the query, 490 * formatted as LIMIT clause. Passing null denotes no LIMIT clause. 491 * @param cancellationSignal A signal to cancel the operation in progress, or null if none. 492 * If the operation is canceled, then {@link OperationCanceledException} will be thrown 493 * when the query is executed. 494 * @return a cursor over the result set 495 * @see android.content.ContentResolver#query(android.net.Uri, String[], 496 * String, String[], String) 497 */ query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit, CancellationSignal cancellationSignal)498 public Cursor query(SQLiteDatabase db, String[] projectionIn, 499 String selection, String[] selectionArgs, String groupBy, 500 String having, String sortOrder, String limit, CancellationSignal cancellationSignal) { 501 if (mTables == null) { 502 return null; 503 } 504 505 final String sql; 506 final String unwrappedSql = buildQuery( 507 projectionIn, selection, groupBy, having, 508 sortOrder, limit); 509 510 if (isStrictColumns()) { 511 enforceStrictColumns(projectionIn); 512 } 513 if (isStrictGrammar()) { 514 enforceStrictGrammar(selection, groupBy, having, sortOrder, limit); 515 } 516 if (isStrict()) { 517 // Validate the user-supplied selection to detect syntactic anomalies 518 // in the selection string that could indicate a SQL injection attempt. 519 // The idea is to ensure that the selection clause is a valid SQL expression 520 // by compiling it twice: once wrapped in parentheses and once as 521 // originally specified. An attacker cannot create an expression that 522 // would escape the SQL expression while maintaining balanced parentheses 523 // in both the wrapped and original forms. 524 525 // NOTE: The ordering of the below operations is important; we must 526 // execute the wrapped query to ensure the untrusted clause has been 527 // fully isolated. 528 529 // Validate the unwrapped query 530 db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid 531 532 // Execute wrapped query for extra protection 533 final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy, 534 wrap(having), sortOrder, limit); 535 sql = wrappedSql; 536 } else { 537 // Execute unwrapped query 538 sql = unwrappedSql; 539 } 540 541 final String[] sqlArgs = selectionArgs; 542 if (Log.isLoggable(TAG, Log.DEBUG)) { 543 if (Build.IS_DEBUGGABLE) { 544 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 545 } else { 546 Log.d(TAG, sql); 547 } 548 } 549 return db.rawQueryWithFactory( 550 mFactory, sql, sqlArgs, 551 SQLiteDatabase.findEditTable(mTables), 552 cancellationSignal); // will throw if query is invalid 553 } 554 555 /** 556 * Perform an update by combining all current settings and the 557 * information passed into this method. 558 * 559 * @param db the database to update on 560 * @param selection A filter declaring which rows to return, 561 * formatted as an SQL WHERE clause (excluding the WHERE 562 * itself). Passing null will return all rows for the given URL. 563 * @param selectionArgs You may include ?s in selection, which 564 * will be replaced by the values from selectionArgs, in order 565 * that they appear in the selection. The values will be bound 566 * as Strings. 567 * @return the number of rows updated 568 * @hide 569 */ update(@onNull SQLiteDatabase db, @NonNull ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)570 public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values, 571 @Nullable String selection, @Nullable String[] selectionArgs) { 572 Objects.requireNonNull(mTables, "No tables defined"); 573 Objects.requireNonNull(db, "No database defined"); 574 Objects.requireNonNull(values, "No values defined"); 575 576 final String sql; 577 final String unwrappedSql = buildUpdate(values, selection); 578 579 if (isStrictColumns()) { 580 enforceStrictColumns(values); 581 } 582 if (isStrictGrammar()) { 583 enforceStrictGrammar(selection, null, null, null, null); 584 } 585 if (isStrict()) { 586 // Validate the user-supplied selection to detect syntactic anomalies 587 // in the selection string that could indicate a SQL injection attempt. 588 // The idea is to ensure that the selection clause is a valid SQL expression 589 // by compiling it twice: once wrapped in parentheses and once as 590 // originally specified. An attacker cannot create an expression that 591 // would escape the SQL expression while maintaining balanced parentheses 592 // in both the wrapped and original forms. 593 594 // NOTE: The ordering of the below operations is important; we must 595 // execute the wrapped query to ensure the untrusted clause has been 596 // fully isolated. 597 598 // Validate the unwrapped query 599 db.validateSql(unwrappedSql, null); // will throw if query is invalid 600 601 // Execute wrapped query for extra protection 602 final String wrappedSql = buildUpdate(values, wrap(selection)); 603 sql = wrappedSql; 604 } else { 605 // Execute unwrapped query 606 sql = unwrappedSql; 607 } 608 609 if (selectionArgs == null) { 610 selectionArgs = EmptyArray.STRING; 611 } 612 final String[] rawKeys = values.keySet().toArray(EmptyArray.STRING); 613 final int valuesLength = rawKeys.length; 614 final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length]; 615 for (int i = 0; i < sqlArgs.length; i++) { 616 if (i < valuesLength) { 617 sqlArgs[i] = values.get(rawKeys[i]); 618 } else { 619 sqlArgs[i] = selectionArgs[i - valuesLength]; 620 } 621 } 622 if (Log.isLoggable(TAG, Log.DEBUG)) { 623 if (Build.IS_DEBUGGABLE) { 624 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 625 } else { 626 Log.d(TAG, sql); 627 } 628 } 629 return db.executeSql(sql, sqlArgs); 630 } 631 632 /** 633 * Perform a delete by combining all current settings and the 634 * information passed into this method. 635 * 636 * @param db the database to delete on 637 * @param selection A filter declaring which rows to return, 638 * formatted as an SQL WHERE clause (excluding the WHERE 639 * itself). Passing null will return all rows for the given URL. 640 * @param selectionArgs You may include ?s in selection, which 641 * will be replaced by the values from selectionArgs, in order 642 * that they appear in the selection. The values will be bound 643 * as Strings. 644 * @return the number of rows deleted 645 * @hide 646 */ delete(@onNull SQLiteDatabase db, @Nullable String selection, @Nullable String[] selectionArgs)647 public int delete(@NonNull SQLiteDatabase db, @Nullable String selection, 648 @Nullable String[] selectionArgs) { 649 Objects.requireNonNull(mTables, "No tables defined"); 650 Objects.requireNonNull(db, "No database defined"); 651 652 final String sql; 653 final String unwrappedSql = buildDelete(selection); 654 655 if (isStrictGrammar()) { 656 enforceStrictGrammar(selection, null, null, null, null); 657 } 658 if (isStrict()) { 659 // Validate the user-supplied selection to detect syntactic anomalies 660 // in the selection string that could indicate a SQL injection attempt. 661 // The idea is to ensure that the selection clause is a valid SQL expression 662 // by compiling it twice: once wrapped in parentheses and once as 663 // originally specified. An attacker cannot create an expression that 664 // would escape the SQL expression while maintaining balanced parentheses 665 // in both the wrapped and original forms. 666 667 // NOTE: The ordering of the below operations is important; we must 668 // execute the wrapped query to ensure the untrusted clause has been 669 // fully isolated. 670 671 // Validate the unwrapped query 672 db.validateSql(unwrappedSql, null); // will throw if query is invalid 673 674 // Execute wrapped query for extra protection 675 final String wrappedSql = buildDelete(wrap(selection)); 676 sql = wrappedSql; 677 } else { 678 // Execute unwrapped query 679 sql = unwrappedSql; 680 } 681 682 final String[] sqlArgs = selectionArgs; 683 if (Log.isLoggable(TAG, Log.DEBUG)) { 684 if (Build.IS_DEBUGGABLE) { 685 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs)); 686 } else { 687 Log.d(TAG, sql); 688 } 689 } 690 return db.executeSql(sql, sqlArgs); 691 } 692 enforceStrictColumns(@ullable String[] projection)693 private void enforceStrictColumns(@Nullable String[] projection) { 694 Objects.requireNonNull(mProjectionMap, "No projection map defined"); 695 696 computeProjection(projection); 697 } 698 enforceStrictColumns(@onNull ContentValues values)699 private void enforceStrictColumns(@NonNull ContentValues values) { 700 Objects.requireNonNull(mProjectionMap, "No projection map defined"); 701 702 final Set<String> rawValues = values.keySet(); 703 final Iterator<String> rawValuesIt = rawValues.iterator(); 704 while (rawValuesIt.hasNext()) { 705 final String column = rawValuesIt.next(); 706 if (!mProjectionMap.containsKey(column)) { 707 throw new IllegalArgumentException("Invalid column " + column); 708 } 709 } 710 } 711 enforceStrictGrammar(@ullable String selection, @Nullable String groupBy, @Nullable String having, @Nullable String sortOrder, @Nullable String limit)712 private void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy, 713 @Nullable String having, @Nullable String sortOrder, @Nullable String limit) { 714 SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE, 715 this::enforceStrictGrammarWhereHaving); 716 SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE, 717 this::enforceStrictGrammarGroupBy); 718 SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE, 719 this::enforceStrictGrammarWhereHaving); 720 SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE, 721 this::enforceStrictGrammarOrderBy); 722 SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE, 723 this::enforceStrictGrammarLimit); 724 } 725 enforceStrictGrammarWhereHaving(@onNull String token)726 private void enforceStrictGrammarWhereHaving(@NonNull String token) { 727 if (isTableOrColumn(token)) return; 728 if (SQLiteTokenizer.isFunction(token)) return; 729 if (SQLiteTokenizer.isType(token)) return; 730 731 // NOTE: we explicitly don't allow SELECT subqueries, since they could 732 // leak data that should have been filtered by the trusted where clause 733 switch (token.toUpperCase(Locale.US)) { 734 case "AND": case "AS": case "BETWEEN": case "BINARY": 735 case "CASE": case "CAST": case "COLLATE": case "DISTINCT": 736 case "ELSE": case "END": case "ESCAPE": case "EXISTS": 737 case "GLOB": case "IN": case "IS": case "ISNULL": 738 case "LIKE": case "MATCH": case "NOCASE": case "NOT": 739 case "NOTNULL": case "NULL": case "OR": case "REGEXP": 740 case "RTRIM": case "THEN": case "WHEN": 741 return; 742 } 743 throw new IllegalArgumentException("Invalid token " + token); 744 } 745 enforceStrictGrammarGroupBy(@onNull String token)746 private void enforceStrictGrammarGroupBy(@NonNull String token) { 747 if (isTableOrColumn(token)) return; 748 throw new IllegalArgumentException("Invalid token " + token); 749 } 750 enforceStrictGrammarOrderBy(@onNull String token)751 private void enforceStrictGrammarOrderBy(@NonNull String token) { 752 if (isTableOrColumn(token)) return; 753 switch (token.toUpperCase(Locale.US)) { 754 case "COLLATE": case "ASC": case "DESC": 755 case "BINARY": case "RTRIM": case "NOCASE": 756 return; 757 } 758 throw new IllegalArgumentException("Invalid token " + token); 759 } 760 enforceStrictGrammarLimit(@onNull String token)761 private void enforceStrictGrammarLimit(@NonNull String token) { 762 switch (token.toUpperCase(Locale.US)) { 763 case "OFFSET": 764 return; 765 } 766 throw new IllegalArgumentException("Invalid token " + token); 767 } 768 769 /** 770 * Construct a SELECT statement suitable for use in a group of 771 * SELECT statements that will be joined through UNION operators 772 * in buildUnionQuery. 773 * 774 * @param projectionIn A list of which columns to return. Passing 775 * null will return all columns, which is discouraged to 776 * prevent reading data from storage that isn't going to be 777 * used. 778 * @param selection A filter declaring which rows to return, 779 * formatted as an SQL WHERE clause (excluding the WHERE 780 * itself). Passing null will return all rows for the given 781 * URL. 782 * @param groupBy A filter declaring how to group rows, formatted 783 * as an SQL GROUP BY clause (excluding the GROUP BY itself). 784 * Passing null will cause the rows to not be grouped. 785 * @param having A filter declare which row groups to include in 786 * the cursor, if row grouping is being used, formatted as an 787 * SQL HAVING clause (excluding the HAVING itself). Passing 788 * null will cause all row groups to be included, and is 789 * required when row grouping is not being used. 790 * @param sortOrder How to order the rows, formatted as an SQL 791 * ORDER BY clause (excluding the ORDER BY itself). Passing null 792 * will use the default sort order, which may be unordered. 793 * @param limit Limits the number of rows returned by the query, 794 * formatted as LIMIT clause. Passing null denotes no LIMIT clause. 795 * @return the resulting SQL SELECT statement 796 */ buildQuery( String[] projectionIn, String selection, String groupBy, String having, String sortOrder, String limit)797 public String buildQuery( 798 String[] projectionIn, String selection, String groupBy, 799 String having, String sortOrder, String limit) { 800 String[] projection = computeProjection(projectionIn); 801 String where = computeWhere(selection); 802 803 return buildQueryString( 804 mDistinct, mTables, projection, where, 805 groupBy, having, sortOrder, limit); 806 } 807 808 /** 809 * @deprecated This method's signature is misleading since no SQL parameter 810 * substitution is carried out. The selection arguments parameter does not get 811 * used at all. To avoid confusion, call 812 * {@link #buildQuery(String[], String, String, String, String, String)} instead. 813 */ 814 @Deprecated buildQuery( String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit)815 public String buildQuery( 816 String[] projectionIn, String selection, String[] selectionArgs, 817 String groupBy, String having, String sortOrder, String limit) { 818 return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit); 819 } 820 821 /** {@hide} */ buildUpdate(ContentValues values, String selection)822 public String buildUpdate(ContentValues values, String selection) { 823 if (values == null || values.size() == 0) { 824 throw new IllegalArgumentException("Empty values"); 825 } 826 827 StringBuilder sql = new StringBuilder(120); 828 sql.append("UPDATE "); 829 sql.append(SQLiteDatabase.findEditTable(mTables)); 830 sql.append(" SET "); 831 832 final String[] rawKeys = values.keySet().toArray(EmptyArray.STRING); 833 for (int i = 0; i < rawKeys.length; i++) { 834 if (i > 0) { 835 sql.append(','); 836 } 837 sql.append(rawKeys[i]); 838 sql.append("=?"); 839 } 840 841 final String where = computeWhere(selection); 842 appendClause(sql, " WHERE ", where); 843 return sql.toString(); 844 } 845 846 /** {@hide} */ buildDelete(String selection)847 public String buildDelete(String selection) { 848 StringBuilder sql = new StringBuilder(120); 849 sql.append("DELETE FROM "); 850 sql.append(SQLiteDatabase.findEditTable(mTables)); 851 852 final String where = computeWhere(selection); 853 appendClause(sql, " WHERE ", where); 854 return sql.toString(); 855 } 856 857 /** 858 * Construct a SELECT statement suitable for use in a group of 859 * SELECT statements that will be joined through UNION operators 860 * in buildUnionQuery. 861 * 862 * @param typeDiscriminatorColumn the name of the result column 863 * whose cells will contain the name of the table from which 864 * each row was drawn. 865 * @param unionColumns the names of the columns to appear in the 866 * result. This may include columns that do not appear in the 867 * table this SELECT is querying (i.e. mTables), but that do 868 * appear in one of the other tables in the UNION query that we 869 * are constructing. 870 * @param columnsPresentInTable a Set of the names of the columns 871 * that appear in this table (i.e. in the table whose name is 872 * mTables). Since columns in unionColumns include columns that 873 * appear only in other tables, we use this array to distinguish 874 * which ones actually are present. Other columns will have 875 * NULL values for results from this subquery. 876 * @param computedColumnsOffset all columns in unionColumns before 877 * this index are included under the assumption that they're 878 * computed and therefore won't appear in columnsPresentInTable, 879 * e.g. "date * 1000 as normalized_date" 880 * @param typeDiscriminatorValue the value used for the 881 * type-discriminator column in this subquery 882 * @param selection A filter declaring which rows to return, 883 * formatted as an SQL WHERE clause (excluding the WHERE 884 * itself). Passing null will return all rows for the given 885 * URL. 886 * @param groupBy A filter declaring how to group rows, formatted 887 * as an SQL GROUP BY clause (excluding the GROUP BY itself). 888 * Passing null will cause the rows to not be grouped. 889 * @param having A filter declare which row groups to include in 890 * the cursor, if row grouping is being used, formatted as an 891 * SQL HAVING clause (excluding the HAVING itself). Passing 892 * null will cause all row groups to be included, and is 893 * required when row grouping is not being used. 894 * @return the resulting SQL SELECT statement 895 */ buildUnionSubQuery( String typeDiscriminatorColumn, String[] unionColumns, Set<String> columnsPresentInTable, int computedColumnsOffset, String typeDiscriminatorValue, String selection, String groupBy, String having)896 public String buildUnionSubQuery( 897 String typeDiscriminatorColumn, 898 String[] unionColumns, 899 Set<String> columnsPresentInTable, 900 int computedColumnsOffset, 901 String typeDiscriminatorValue, 902 String selection, 903 String groupBy, 904 String having) { 905 int unionColumnsCount = unionColumns.length; 906 String[] projectionIn = new String[unionColumnsCount]; 907 908 for (int i = 0; i < unionColumnsCount; i++) { 909 String unionColumn = unionColumns[i]; 910 911 if (unionColumn.equals(typeDiscriminatorColumn)) { 912 projectionIn[i] = "'" + typeDiscriminatorValue + "' AS " 913 + typeDiscriminatorColumn; 914 } else if (i <= computedColumnsOffset 915 || columnsPresentInTable.contains(unionColumn)) { 916 projectionIn[i] = unionColumn; 917 } else { 918 projectionIn[i] = "NULL AS " + unionColumn; 919 } 920 } 921 return buildQuery( 922 projectionIn, selection, groupBy, having, 923 null /* sortOrder */, 924 null /* limit */); 925 } 926 927 /** 928 * @deprecated This method's signature is misleading since no SQL parameter 929 * substitution is carried out. The selection arguments parameter does not get 930 * used at all. To avoid confusion, call 931 * {@link #buildUnionSubQuery} 932 * instead. 933 */ 934 @Deprecated buildUnionSubQuery( String typeDiscriminatorColumn, String[] unionColumns, Set<String> columnsPresentInTable, int computedColumnsOffset, String typeDiscriminatorValue, String selection, String[] selectionArgs, String groupBy, String having)935 public String buildUnionSubQuery( 936 String typeDiscriminatorColumn, 937 String[] unionColumns, 938 Set<String> columnsPresentInTable, 939 int computedColumnsOffset, 940 String typeDiscriminatorValue, 941 String selection, 942 String[] selectionArgs, 943 String groupBy, 944 String having) { 945 return buildUnionSubQuery( 946 typeDiscriminatorColumn, unionColumns, columnsPresentInTable, 947 computedColumnsOffset, typeDiscriminatorValue, selection, 948 groupBy, having); 949 } 950 951 /** 952 * Given a set of subqueries, all of which are SELECT statements, 953 * construct a query that returns the union of what those 954 * subqueries return. 955 * @param subQueries an array of SQL SELECT statements, all of 956 * which must have the same columns as the same positions in 957 * their results 958 * @param sortOrder How to order the rows, formatted as an SQL 959 * ORDER BY clause (excluding the ORDER BY itself). Passing 960 * null will use the default sort order, which may be unordered. 961 * @param limit The limit clause, which applies to the entire union result set 962 * 963 * @return the resulting SQL SELECT statement 964 */ buildUnionQuery(String[] subQueries, String sortOrder, String limit)965 public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) { 966 StringBuilder query = new StringBuilder(128); 967 int subQueryCount = subQueries.length; 968 String unionOperator = mDistinct ? " UNION " : " UNION ALL "; 969 970 for (int i = 0; i < subQueryCount; i++) { 971 if (i > 0) { 972 query.append(unionOperator); 973 } 974 query.append(subQueries[i]); 975 } 976 appendClause(query, " ORDER BY ", sortOrder); 977 appendClause(query, " LIMIT ", limit); 978 return query.toString(); 979 } 980 maybeWithOperator(@ullable String operator, @NonNull String column)981 private static @NonNull String maybeWithOperator(@Nullable String operator, 982 @NonNull String column) { 983 if (operator != null) { 984 return operator + "(" + column + ")"; 985 } else { 986 return column; 987 } 988 } 989 990 /** {@hide} */ computeProjection(@ullable String[] projectionIn)991 public @Nullable String[] computeProjection(@Nullable String[] projectionIn) { 992 if (!ArrayUtils.isEmpty(projectionIn)) { 993 String[] projectionOut = new String[projectionIn.length]; 994 for (int i = 0; i < projectionIn.length; i++) { 995 projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]); 996 } 997 return projectionOut; 998 } else if (mProjectionMap != null) { 999 // Return all columns in projection map. 1000 Set<Entry<String, String>> entrySet = mProjectionMap.entrySet(); 1001 String[] projection = new String[entrySet.size()]; 1002 Iterator<Entry<String, String>> entryIter = entrySet.iterator(); 1003 int i = 0; 1004 1005 while (entryIter.hasNext()) { 1006 Entry<String, String> entry = entryIter.next(); 1007 1008 // Don't include the _count column when people ask for no projection. 1009 if (entry.getKey().equals(BaseColumns._COUNT)) { 1010 continue; 1011 } 1012 projection[i++] = entry.getValue(); 1013 } 1014 return projection; 1015 } 1016 return null; 1017 } 1018 computeSingleProjectionOrThrow(@onNull String userColumn)1019 private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) { 1020 final String column = computeSingleProjection(userColumn); 1021 if (column != null) { 1022 return column; 1023 } else { 1024 throw new IllegalArgumentException("Invalid column " + userColumn); 1025 } 1026 } 1027 computeSingleProjection(@onNull String userColumn)1028 private @Nullable String computeSingleProjection(@NonNull String userColumn) { 1029 // When no mapping provided, anything goes 1030 if (mProjectionMap == null) { 1031 return userColumn; 1032 } 1033 1034 String operator = null; 1035 String column = mProjectionMap.get(userColumn); 1036 1037 // When no direct match found, look for aggregation 1038 if (column == null) { 1039 final Matcher matcher = sAggregationPattern.matcher(userColumn); 1040 if (matcher.matches()) { 1041 operator = matcher.group(1); 1042 userColumn = matcher.group(2); 1043 column = mProjectionMap.get(userColumn); 1044 } 1045 } 1046 1047 if (column != null) { 1048 return maybeWithOperator(operator, column); 1049 } 1050 1051 if (mStrictFlags == 0 1052 && (userColumn.contains(" AS ") || userColumn.contains(" as "))) { 1053 /* A column alias already exist */ 1054 return maybeWithOperator(operator, userColumn); 1055 } 1056 1057 // If greylist is configured, we might be willing to let 1058 // this custom column bypass our strict checks. 1059 if (mProjectionGreylist != null) { 1060 boolean match = false; 1061 for (Pattern p : mProjectionGreylist) { 1062 if (p.matcher(userColumn).matches()) { 1063 match = true; 1064 break; 1065 } 1066 } 1067 1068 if (match) { 1069 Log.w(TAG, "Allowing abusive custom column: " + userColumn); 1070 return maybeWithOperator(operator, userColumn); 1071 } 1072 } 1073 1074 return null; 1075 } 1076 isTableOrColumn(String token)1077 private boolean isTableOrColumn(String token) { 1078 if (mTables.equals(token)) return true; 1079 return computeSingleProjection(token) != null; 1080 } 1081 1082 /** {@hide} */ computeWhere(@ullable String selection)1083 public @Nullable String computeWhere(@Nullable String selection) { 1084 final boolean hasInternal = !TextUtils.isEmpty(mWhereClause); 1085 final boolean hasExternal = !TextUtils.isEmpty(selection); 1086 1087 if (hasInternal || hasExternal) { 1088 final StringBuilder where = new StringBuilder(); 1089 if (hasInternal) { 1090 where.append('(').append(mWhereClause).append(')'); 1091 } 1092 if (hasInternal && hasExternal) { 1093 where.append(" AND "); 1094 } 1095 if (hasExternal) { 1096 where.append('(').append(selection).append(')'); 1097 } 1098 return where.toString(); 1099 } else { 1100 return null; 1101 } 1102 } 1103 1104 /** 1105 * Wrap given argument in parenthesis, unless it's {@code null} or 1106 * {@code ()}, in which case return it verbatim. 1107 */ wrap(@ullable String arg)1108 private @Nullable String wrap(@Nullable String arg) { 1109 if (TextUtils.isEmpty(arg)) { 1110 return arg; 1111 } else { 1112 return "(" + arg + ")"; 1113 } 1114 } 1115 } 1116