• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;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 (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;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