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 com.android.providers.media.util;
18 
19 import static android.content.ContentResolver.QUERY_ARG_SQL_GROUP_BY;
20 import static android.content.ContentResolver.QUERY_ARG_SQL_HAVING;
21 import static android.content.ContentResolver.QUERY_ARG_SQL_LIMIT;
22 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION;
23 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS;
24 import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER;
25 
26 import static com.android.providers.media.util.DatabaseUtils.bindSelection;
27 
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.content.ContentValues;
31 import android.database.Cursor;
32 import android.database.DatabaseUtils;
33 import android.database.sqlite.SQLiteDatabase;
34 import android.os.Build;
35 import android.os.Bundle;
36 import android.os.CancellationSignal;
37 import android.os.OperationCanceledException;
38 import android.provider.BaseColumns;
39 import android.provider.MediaStore.MediaColumns;
40 import android.text.TextUtils;
41 import android.util.ArrayMap;
42 import android.util.Log;
43 
44 import androidx.annotation.VisibleForTesting;
45 
46 import com.android.providers.media.DatabaseHelper;
47 
48 import com.google.common.base.Strings;
49 
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.Collection;
53 import java.util.Iterator;
54 import java.util.Locale;
55 import java.util.Map;
56 import java.util.Map.Entry;
57 import java.util.Objects;
58 import java.util.Set;
59 import java.util.regex.Matcher;
60 import java.util.regex.Pattern;
61 
62 /**
63  * This is a convenience class that helps build SQL queries to be sent to
64  * {@link SQLiteDatabase} objects.
65  */
66 public class SQLiteQueryBuilder {
67 
68     public static final String ROWID_COLUMN = "rowid";
69 
70     private static final String TAG = "SQLiteQueryBuilder";
71 
72     private static final Pattern sAggregationPattern = Pattern.compile(
73             "(?i)(AVG|COUNT|MAX|MIN|SUM|TOTAL|GROUP_CONCAT|UNICODE)\\((.+)\\)");
74 
75     /**
76      * Narrow concessions to support legacy apps that aren't using proper SQL
77      * string substitution; these values come from specific bugs.
78      */
79     private static final Pattern sPattern154193772 = Pattern.compile(
80             "(?i)%\\.(wmv|wm|wtv|asf|hls|mp4|m4v|mov|mp4v|3g2|3gp|3gp2|3gpp|mj2|qt|external|"
81                     + "mov|asf|avi|divx|mpg|mpeg|mkv|webm|mk3d|mks|3gp|mpegts|ts|m2ts|m2t)");
82     private static final Pattern sPattern156832140 = Pattern.compile(
83             "(?i)%com\\.gopro\\.smarty%");
84     private static final Pattern sPattern158537159 = Pattern.compile("(?i)localized");
85 
86     private static final Pattern sCustomCollatorPattern = Pattern.compile(
87             "(?i)custom_[a-zA-Z]+");
88 
89     private Map<String, String> mProjectionMap = null;
90     private Collection<Pattern> mProjectionAllowlist = null;
91 
92     private String mTables = "";
93     private StringBuilder mWhereClause = null;  // lazily created
94     private boolean mDistinct;
95 
96     private static final int STRICT_PARENTHESES = 1 << 0;
97     private static final int STRICT_COLUMNS = 1 << 1;
98     private static final int STRICT_GRAMMAR = 1 << 2;
99 
100     private int mStrictFlags;
101 
102     private int mTargetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
103 
setTargetSdkVersion(int targetSdkVersion)104     public void setTargetSdkVersion(int targetSdkVersion) {
105         mTargetSdkVersion = targetSdkVersion;
106     }
107 
108     /**
109      * Raw SQL clause to obtain the value of {@link MediaColumns#_ID} from custom database function
110      * {@code _GET_ID} for INSERT operation.
111      */
112     private static final String GET_ID_FOR_INSERT_CLAUSE = "_GET_ID(?)";
113 
114     /**
115      * Raw SQL clause to obtain the value of {@link MediaColumns#_ID} from custom database function
116      * {@code _GET_ID} for UPDATE operation.
117      */
118     private static final String GET_ID_FOR_UPDATE_CLAUSE = "ifnull(_GET_ID(?), _id)";
119 
SQLiteQueryBuilder()120     public SQLiteQueryBuilder() {
121         mDistinct = false;
122     }
123 
124     /**
125      * Constructs a new SQLiteQueryBuilder instance as a copy of existing one.
126      *
127      * @param qb an instance to copy.
128      */
SQLiteQueryBuilder(SQLiteQueryBuilder qb)129     public SQLiteQueryBuilder(SQLiteQueryBuilder qb) {
130         if (qb != null) {
131             if (qb.mProjectionMap != null) {
132                 final ArrayMap<String, String> projectionMapCopy = new ArrayMap<>();
133                 projectionMapCopy.putAll(qb.mProjectionMap);
134                 this.mProjectionMap = projectionMapCopy;
135             }
136 
137             if (qb.mProjectionAllowlist != null) {
138                 this.mProjectionAllowlist = new ArrayList<>(qb.mProjectionAllowlist);
139             }
140 
141             if (qb.mWhereClause != null) {
142                 this.mWhereClause = new StringBuilder(qb.mWhereClause);
143             }
144 
145             this.mTables = qb.mTables;
146             this.mDistinct = qb.mDistinct;
147             this.mStrictFlags = qb.mStrictFlags;
148             this.mTargetSdkVersion = qb.mTargetSdkVersion;
149         }
150     }
151 
152     /**
153      * Mark the query as {@code DISTINCT}.
154      *
155      * @param distinct if true the query is {@code DISTINCT}, otherwise it isn't
156      */
setDistinct(boolean distinct)157     public void setDistinct(boolean distinct) {
158         mDistinct = distinct;
159     }
160 
161     /**
162      * Get if the query is marked as {@code DISTINCT}, as last configured by
163      * {@link #setDistinct(boolean)}.
164      */
isDistinct()165     public boolean isDistinct() {
166         return mDistinct;
167     }
168 
169     /**
170      * Returns the list of tables being queried
171      *
172      * @return the list of tables being queried
173      */
getTables()174     public @Nullable String getTables() {
175         return mTables;
176     }
177 
178     /**
179      * Sets the list of tables to query. Multiple tables can be specified to perform a join.
180      * For example:
181      *   setTables("foo, bar")
182      *   setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)")
183      *
184      * @param inTables the list of tables to query on
185      */
setTables(@ullable String inTables)186     public void setTables(@Nullable String inTables) {
187         mTables = inTables;
188     }
189 
190     /**
191      * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded
192      * by parenthesis and {@code AND}ed with the selection passed to {@link #query}. The final
193      * {@code WHERE} clause looks like:
194      * <p>
195      * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
196      *
197      * @param inWhere the chunk of text to append to the {@code WHERE} clause.
198      */
appendWhere(@onNull CharSequence inWhere)199     public void appendWhere(@NonNull CharSequence inWhere) {
200         if (mWhereClause == null) {
201             mWhereClause = new StringBuilder(inWhere.length() + 16);
202         }
203         mWhereClause.append(inWhere);
204     }
205 
206     /**
207      * Append a chunk to the {@code WHERE} clause of the query. All chunks appended are surrounded
208      * by parenthesis and ANDed with the selection passed to {@link #query}. The final
209      * {@code WHERE} clause looks like:
210      * <p>
211      * WHERE (&lt;append chunk 1>&lt;append chunk2>) AND (&lt;query() selection parameter>)
212      *
213      * @param inWhere the chunk of text to append to the {@code WHERE} clause. it will be escaped
214      * to avoid SQL injection attacks
215      */
appendWhereEscapeString(@onNull String inWhere)216     public void appendWhereEscapeString(@NonNull String inWhere) {
217         if (mWhereClause == null) {
218             mWhereClause = new StringBuilder(inWhere.length() + 16);
219         }
220         DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere);
221     }
222 
223     /**
224      * Add a standalone chunk to the {@code WHERE} clause of this query.
225      * <p>
226      * This method differs from {@link #appendWhere(CharSequence)} in that it
227      * automatically appends {@code AND} to any existing {@code WHERE} clause
228      * already under construction before appending the given standalone
229      * expression wrapped in parentheses.
230      *
231      * @param inWhere the standalone expression to append to the {@code WHERE}
232      *            clause. It will be wrapped in parentheses when it's appended.
233      */
appendWhereStandalone(@onNull CharSequence inWhere)234     public void appendWhereStandalone(@NonNull CharSequence inWhere) {
235         if (mWhereClause == null) {
236             mWhereClause = new StringBuilder(inWhere.length() + 16);
237         }
238         if (mWhereClause.length() > 0) {
239             mWhereClause.append(" AND ");
240         }
241         mWhereClause.append('(').append(inWhere).append(')');
242     }
243 
244     /**
245      * Sets the projection map for the query.  The projection map maps
246      * from column names that the caller passes into query to database
247      * column names. This is useful for renaming columns as well as
248      * disambiguating column names when doing joins. For example you
249      * could map "name" to "people.name".  If a projection map is set
250      * it must contain all column names the user may request, even if
251      * the key and value are the same.
252      *
253      * @param columnMap maps from the user column names to the database column names
254      */
setProjectionMap(@ullable Map<String, String> columnMap)255     public void setProjectionMap(@Nullable Map<String, String> columnMap) {
256         if (columnMap != null) {
257             mProjectionMap = new ArrayMap<String, String>();
258             for (Entry<String, String> entry : columnMap.entrySet()) {
259                 mProjectionMap.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue());
260             }
261         } else {
262             mProjectionMap = null;
263         }
264     }
265 
266     /** Adds {@code column} to the projection map. */
allowColumn(String column)267     public void allowColumn(String column) {
268         if (mProjectionMap == null) {
269             mProjectionMap = new ArrayMap<>();
270         }
271         mProjectionMap.put(column, column);
272     }
273 
274     /**
275      * Gets the projection map for the query, as last configured by
276      * {@link #setProjectionMap(Map)}.
277      */
getProjectionMap()278     public @Nullable Map<String, String> getProjectionMap() {
279         return mProjectionMap;
280     }
281 
282     /**
283      * Sets a projection allowlist of columns that will be allowed through, even
284      * when {@link #setStrict(boolean)} is enabled. This provides a way for
285      * abusive custom columns like {@code COUNT(*)} to continue working.
286      */
setProjectionAllowlist(@ullable Collection<Pattern> projectionAllowlist)287     public void setProjectionAllowlist(@Nullable Collection<Pattern> projectionAllowlist) {
288         mProjectionAllowlist = projectionAllowlist;
289     }
290 
291     /**
292      * Gets the projection allowlist for the query, as last configured by
293      * {@link #setProjectionAllowlist}.
294      */
getProjectionAllowlist()295     public @Nullable Collection<Pattern> getProjectionAllowlist() {
296         return mProjectionAllowlist;
297     }
298 
299     /**
300      * When set, the selection is verified against malicious arguments. When
301      * using this class to create a statement using
302      * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)},
303      * non-numeric limits will raise an exception. If a projection map is
304      * specified, fields not in that map will be ignored. If this class is used
305      * to execute the statement directly using
306      * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)}
307      * or
308      * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)},
309      * additionally also parenthesis escaping selection are caught. To
310      * summarize: To get maximum protection against malicious third party apps
311      * (for example content provider consumers), make sure to do the following:
312      * <ul>
313      * <li>Set this value to true</li>
314      * <li>Use a projection map</li>
315      * <li>Use one of the query overloads instead of getting the statement as a
316      * sql string</li>
317      * </ul>
318      * <p>
319      * This feature is disabled by default on each newly constructed
320      * {@link SQLiteQueryBuilder} and needs to be manually enabled.
321      */
setStrict(boolean strict)322     public void setStrict(boolean strict) {
323         if (strict) {
324             mStrictFlags |= STRICT_PARENTHESES;
325         } else {
326             mStrictFlags &= ~STRICT_PARENTHESES;
327         }
328     }
329 
330     /**
331      * Get if the query is marked as strict, as last configured by
332      * {@link #setStrict(boolean)}.
333      */
isStrict()334     public boolean isStrict() {
335         return (mStrictFlags & STRICT_PARENTHESES) != 0;
336     }
337 
338     /**
339      * When enabled, verify that all projections and {@link ContentValues} only
340      * contain valid columns as defined by {@link #setProjectionMap(Map)}.
341      * <p>
342      * This enforcement applies to {@link #insert}, {@link #query}, and
343      * {@link #update} operations. Any enforcement failures will throw an
344      * {@link IllegalArgumentException}.
345      * <p>
346      * This feature is disabled by default on each newly constructed
347      * {@link SQLiteQueryBuilder} and needs to be manually enabled.
348      */
setStrictColumns(boolean strictColumns)349     public void setStrictColumns(boolean strictColumns) {
350         if (strictColumns) {
351             mStrictFlags |= STRICT_COLUMNS;
352         } else {
353             mStrictFlags &= ~STRICT_COLUMNS;
354         }
355     }
356 
357     /**
358      * Get if the query is marked as strict, as last configured by
359      * {@link #setStrictColumns(boolean)}.
360      */
isStrictColumns()361     public boolean isStrictColumns() {
362         return (mStrictFlags & STRICT_COLUMNS) != 0;
363     }
364 
365     /**
366      * When enabled, verify that all untrusted SQL conforms to a restricted SQL
367      * grammar. Here are the restrictions applied:
368      * <ul>
369      * <li>In {@code WHERE} and {@code HAVING} clauses: subqueries, raising, and
370      * windowing terms are rejected.
371      * <li>In {@code GROUP BY} clauses: only valid columns are allowed.
372      * <li>In {@code ORDER BY} clauses: only valid columns, collation, and
373      * ordering terms are allowed.
374      * <li>In {@code LIMIT} clauses: only numerical values and offset terms are
375      * allowed.
376      * </ul>
377      * All column references must be valid as defined by
378      * {@link #setProjectionMap(Map)}.
379      * <p>
380      * This enforcement applies to {@link #query}, {@link #update} and
381      * {@link #delete} operations. This enforcement does not apply to trusted
382      * inputs, such as those provided by {@link #appendWhere}. Any enforcement
383      * failures will throw an {@link IllegalArgumentException}.
384      * <p>
385      * This feature is disabled by default on each newly constructed
386      * {@link SQLiteQueryBuilder} and needs to be manually enabled.
387      */
setStrictGrammar(boolean strictGrammar)388     public void setStrictGrammar(boolean strictGrammar) {
389         if (strictGrammar) {
390             mStrictFlags |= STRICT_GRAMMAR;
391         } else {
392             mStrictFlags &= ~STRICT_GRAMMAR;
393         }
394     }
395 
396     /**
397      * Get if the query is marked as strict, as last configured by
398      * {@link #setStrictGrammar(boolean)}.
399      */
isStrictGrammar()400     public boolean isStrictGrammar() {
401         return (mStrictFlags & STRICT_GRAMMAR) != 0;
402     }
403 
404     /**
405      * Build an SQL query string from the given clauses.
406      *
407      * @param distinct true if you want each row to be unique, false otherwise.
408      * @param tables The table names to compile the query against.
409      * @param columns A list of which columns to return. Passing null will
410      *            return all columns, which is discouraged to prevent reading
411      *            data from storage that isn't going to be used.
412      * @param where A filter declaring which rows to return, formatted as an SQL
413      *            {@code WHERE} clause (excluding the {@code WHERE} itself). Passing {@code null} will
414      *            return all rows for the given URL.
415      * @param groupBy A filter declaring how to group rows, formatted as an SQL
416      *            {@code GROUP BY} clause (excluding the {@code GROUP BY} itself). Passing {@code null}
417      *            will cause the rows to not be grouped.
418      * @param having A filter declare which row groups to include in the cursor,
419      *            if row grouping is being used, formatted as an SQL {@code HAVING}
420      *            clause (excluding the {@code HAVING} itself). Passing null will cause
421      *            all row groups to be included, and is required when row
422      *            grouping is not being used.
423      * @param orderBy How to order the rows, formatted as an SQL {@code ORDER BY} clause
424      *            (excluding the {@code ORDER BY} itself). Passing null will use the
425      *            default sort order, which may be unordered.
426      * @param limit Limits the number of rows returned by the query,
427      *            formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
428      * @return the SQL query string
429      */
buildQueryString( boolean distinct, String tables, String[] columns, String where, String groupBy, String having, String orderBy, String limit)430     public static String buildQueryString(
431             boolean distinct, String tables, String[] columns, String where,
432             String groupBy, String having, String orderBy, String limit) {
433         if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) {
434             throw new IllegalArgumentException(
435                     "HAVING clauses are only permitted when using a groupBy clause");
436         }
437 
438         StringBuilder query = new StringBuilder(120);
439 
440         query.append("SELECT ");
441         if (distinct) {
442             query.append("DISTINCT ");
443         }
444         if (columns != null && columns.length != 0) {
445             appendColumns(query, columns);
446         } else {
447             query.append("* ");
448         }
449         query.append("FROM ");
450         query.append(tables);
451         appendClause(query, " WHERE ", where);
452         appendClause(query, " GROUP BY ", groupBy);
453         appendClause(query, " HAVING ", having);
454         appendClause(query, " ORDER BY ", orderBy);
455         appendClause(query, " LIMIT ", limit);
456 
457         return query.toString();
458     }
459 
appendClause(StringBuilder s, String name, String clause)460     private static void appendClause(StringBuilder s, String name, String clause) {
461         if (!TextUtils.isEmpty(clause)) {
462             s.append(name);
463             s.append(clause);
464         }
465     }
466 
467     /**
468      * Add the names that are non-null in columns to s, separating
469      * them with commas.
470      */
appendColumns(StringBuilder s, String[] columns)471     public static void appendColumns(StringBuilder s, String[] columns) {
472         int n = columns.length;
473 
474         for (int i = 0; i < n; i++) {
475             String column = columns[i];
476 
477             if (column != null) {
478                 if (i > 0) {
479                     s.append(", ");
480                 }
481                 s.append(column);
482             }
483         }
484         s.append(' ');
485     }
486 
query(DatabaseHelper helper, String[] projectionIn, Bundle queryArgs, CancellationSignal cancellationSignal)487     public Cursor query(DatabaseHelper helper, String[] projectionIn, Bundle queryArgs,
488             CancellationSignal cancellationSignal) {
489         final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION);
490         final String[] selectionArgs = queryArgs.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
491         final String groupBy = queryArgs.getString(QUERY_ARG_SQL_GROUP_BY);
492         final String having = queryArgs.getString(QUERY_ARG_SQL_HAVING);
493         final String sortOrder = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER);
494         final String limit = queryArgs.getString(QUERY_ARG_SQL_LIMIT);
495         return query(helper, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
496                 limit, cancellationSignal);
497     }
498 
query(DatabaseHelper helper, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit, CancellationSignal cancellationSignal)499     public Cursor query(DatabaseHelper helper, String[] projectionIn,
500             String selection, String[] selectionArgs, String groupBy,
501             String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
502         return helper.runWithoutTransaction((db) -> {
503             return query(db, projectionIn, selection, selectionArgs, groupBy,
504                     having, sortOrder, limit, cancellationSignal);
505         });
506     }
507 
508     /**
509      * Perform a query by combining all current settings and the
510      * information passed into this method.
511      *
512      * @param db the database to query on
513      * @param projectionIn A list of which columns to return. Passing
514      *   null will return all columns, which is discouraged to prevent
515      *   reading data from storage that isn't going to be used.
516      * @param selection A filter declaring which rows to return,
517      *   formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
518      *   itself). Passing null will return all rows for the given URL.
519      * @param selectionArgs You may include ?s in selection, which
520      *   will be replaced by the values from selectionArgs, in order
521      *   that they appear in the selection. The values will be bound
522      *   as Strings.
523      * @param groupBy A filter declaring how to group rows, formatted
524      *   as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
525      *   itself). Passing null will cause the rows to not be grouped.
526      * @param having A filter declare which row groups to include in
527      *   the cursor, if row grouping is being used, formatted as an
528      *   SQL {@code HAVING} clause (excluding the {@code HAVING} itself).  Passing
529      *   null will cause all row groups to be included, and is
530      *   required when row grouping is not being used.
531      * @param sortOrder How to order the rows, formatted as an SQL
532      *   {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
533      *   will use the default sort order, which may be unordered.
534      * @param limit Limits the number of rows returned by the query,
535      *   formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
536      * @param cancellationSignal A signal to cancel the operation in progress, or null if none.
537      * If the operation is canceled, then {@link OperationCanceledException} will be thrown
538      * when the query is executed.
539      * @return a cursor over the result set
540      * @see android.content.ContentResolver#query(android.net.Uri, String[],
541      *      String, String[], String)
542      */
query(SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit, CancellationSignal cancellationSignal)543     public Cursor query(SQLiteDatabase db, String[] projectionIn,
544             String selection, String[] selectionArgs, String groupBy,
545             String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
546         if (mTables == null) {
547             return null;
548         }
549 
550         final String sql;
551         final String unwrappedSql = buildQuery(
552                 projectionIn, selection, groupBy, having,
553                 sortOrder, limit);
554 
555         if (isStrictColumns()) {
556             enforceStrictColumns(projectionIn);
557         }
558         if (isStrictGrammar()) {
559             enforceStrictGrammar(selection, groupBy, having, sortOrder, limit);
560         }
561         if (isStrict() && hasUserWhere(selection)) {
562             // Validate the user-supplied selection to detect syntactic anomalies
563             // in the selection string that could indicate a SQL injection attempt.
564             // The idea is to ensure that the selection clause is a valid SQL expression
565             // by compiling it twice: once wrapped in parentheses and once as
566             // originally specified. An attacker cannot create an expression that
567             // would escape the SQL expression while maintaining balanced parentheses
568             // in both the wrapped and original forms.
569 
570             // NOTE: The ordering of the below operations is important; we must
571             // execute the wrapped query to ensure the untrusted clause has been
572             // fully isolated.
573 
574             // Validate the unwrapped query
575             db.validateSql(unwrappedSql, cancellationSignal); // will throw if query is invalid
576 
577             // Execute wrapped query for extra protection
578             final String wrappedSql = buildQuery(projectionIn, wrap(selection), groupBy,
579                     wrap(having), sortOrder, limit);
580             sql = wrappedSql;
581         } else {
582             // Execute unwrapped query
583             sql = unwrappedSql;
584         }
585 
586         final String[] sqlArgs = selectionArgs;
587         if (Log.isLoggable(TAG, Log.DEBUG)) {
588             if (Logging.IS_DEBUGGABLE) {
589                 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
590             } else {
591                 Log.d(TAG, sql);
592             }
593         }
594         return db.rawQueryWithFactory(
595                 null, sql, sqlArgs,
596                 SQLiteDatabase.findEditTable(mTables),
597                 cancellationSignal); // will throw if query is invalid
598     }
599 
insert(@onNull DatabaseHelper helper, @NonNull ContentValues values)600     public long insert(@NonNull DatabaseHelper helper, @NonNull ContentValues values) {
601         // We force wrap in a transaction to ensure that all mutations increment
602         // the generation counter
603         return helper.runWithTransaction((db) -> {
604             return insert(db, values);
605         });
606     }
607 
608     /**
609      * Perform an insert by combining all current settings and the
610      * information passed into this method.
611      *
612      * @param db the database to insert on
613      * @return the row ID of the newly inserted row, or -1 if an error occurred
614      */
615     public long insert(@NonNull SQLiteDatabase db, @NonNull ContentValues values) {
616         Objects.requireNonNull(mTables, "No tables defined");
617         Objects.requireNonNull(db, "No database defined");
618         Objects.requireNonNull(values, "No values defined");
619 
620         if (isStrictColumns()) {
621             enforceStrictColumns(values);
622         }
623 
624         final String sql = buildInsert(values);
625 
626         final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
627                 .getValues(values);
628         final int valuesLength = rawValues.size();
629         final Object[] sqlArgs = new Object[valuesLength];
630         for (int i = 0; i < sqlArgs.length; i++) {
631             sqlArgs[i] = rawValues.valueAt(i);
632         }
633         if (Log.isLoggable(TAG, Log.DEBUG)) {
634             if (Logging.IS_DEBUGGABLE) {
635                 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
636             } else {
637                 Log.d(TAG, sql);
638             }
639         }
640         return com.android.providers.media.util.DatabaseUtils.executeInsert(db, sql, sqlArgs);
641     }
642 
643     public int update(@NonNull DatabaseHelper helper, @NonNull ContentValues values,
644             @Nullable String selection, @Nullable String[] selectionArgs) {
645         // We force wrap in a transaction to ensure that all mutations increment
646         // the generation counter
647         return helper.runWithTransaction((db) -> {
648             return update(db, values, selection, selectionArgs);
649         });
650     }
651 
652     /**
653      * Perform an update by combining all current settings and the
654      * information passed into this method.
655      *
656      * @param db the database to update on
657      * @param selection A filter declaring which rows to return,
658      *   formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
659      *   itself). Passing null will return all rows for the given URL.
660      * @param selectionArgs You may include ?s in selection, which
661      *   will be replaced by the values from selectionArgs, in order
662      *   that they appear in the selection. The values will be bound
663      *   as Strings.
664      * @return the number of rows updated
665      */
666     public int update(@NonNull SQLiteDatabase db, @NonNull ContentValues values,
667             @Nullable String selection, @Nullable String[] selectionArgs) {
668         Objects.requireNonNull(mTables, "No tables defined");
669         Objects.requireNonNull(db, "No database defined");
670         Objects.requireNonNull(values, "No values defined");
671 
672         final String sql;
673         final String unwrappedSql = buildUpdate(values, selection);
674 
675         if (isStrictColumns()) {
676             enforceStrictColumns(values);
677         }
678         if (isStrictGrammar()) {
679             enforceStrictGrammar(selection, null, null, null, null);
680         }
681         if (isStrict() && hasUserWhere(selection)) {
682             // Validate the user-supplied selection to detect syntactic anomalies
683             // in the selection string that could indicate a SQL injection attempt.
684             // The idea is to ensure that the selection clause is a valid SQL expression
685             // by compiling it twice: once wrapped in parentheses and once as
686             // originally specified. An attacker cannot create an expression that
687             // would escape the SQL expression while maintaining balanced parentheses
688             // in both the wrapped and original forms.
689 
690             // NOTE: The ordering of the below operations is important; we must
691             // execute the wrapped query to ensure the untrusted clause has been
692             // fully isolated.
693 
694             // Validate the unwrapped query
695             db.validateSql(unwrappedSql, null); // will throw if query is invalid
696 
697             // Execute wrapped query for extra protection
698             final String wrappedSql = buildUpdate(values, wrap(selection));
699             sql = wrappedSql;
700         } else {
701             // Execute unwrapped query
702             sql = unwrappedSql;
703         }
704 
705         if (selectionArgs == null) {
706             selectionArgs = new String[0];
707         }
708         final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
709                 .getValues(values);
710         final int valuesLength = rawValues.size();
711         final Object[] sqlArgs = new Object[valuesLength + selectionArgs.length];
712         for (int i = 0; i < sqlArgs.length; i++) {
713             if (i < valuesLength) {
714                 sqlArgs[i] = rawValues.valueAt(i);
715             } else {
716                 sqlArgs[i] = selectionArgs[i - valuesLength];
717             }
718         }
719         if (Log.isLoggable(TAG, Log.DEBUG)) {
720             if (Logging.IS_DEBUGGABLE) {
721                 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
722             } else {
723                 Log.d(TAG, sql);
724             }
725         }
726         return com.android.providers.media.util.DatabaseUtils.executeUpdateDelete(db, sql, sqlArgs);
727     }
728 
729     public int delete(@NonNull DatabaseHelper helper, @Nullable String selection,
730             @Nullable String[] selectionArgs) {
731         // We force wrap in a transaction to ensure that all mutations increment
732         // the generation counter
733         return helper.runWithTransaction((db) -> {
734             return delete(db, selection, selectionArgs);
735         });
736     }
737 
738     /**
739      * Perform a delete by combining all current settings and the
740      * information passed into this method.
741      *
742      * @param db the database to delete on
743      * @param selection A filter declaring which rows to return,
744      *   formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
745      *   itself). Passing null will return all rows for the given URL.
746      * @param selectionArgs You may include ?s in selection, which
747      *   will be replaced by the values from selectionArgs, in order
748      *   that they appear in the selection. The values will be bound
749      *   as Strings.
750      * @return the number of rows deleted
751      */
752     public int delete(@NonNull SQLiteDatabase db, @Nullable String selection,
753             @Nullable String[] selectionArgs) {
754         Objects.requireNonNull(mTables, "No tables defined");
755         Objects.requireNonNull(db, "No database defined");
756 
757         final String sql;
758         final String unwrappedSql = buildDelete(selection);
759 
760         if (isStrictGrammar()) {
761             enforceStrictGrammar(selection, null, null, null, null);
762         }
763         if (isStrict() && hasUserWhere(selection)) {
764             // Validate the user-supplied selection to detect syntactic anomalies
765             // in the selection string that could indicate a SQL injection attempt.
766             // The idea is to ensure that the selection clause is a valid SQL expression
767             // by compiling it twice: once wrapped in parentheses and once as
768             // originally specified. An attacker cannot create an expression that
769             // would escape the SQL expression while maintaining balanced parentheses
770             // in both the wrapped and original forms.
771 
772             // NOTE: The ordering of the below operations is important; we must
773             // execute the wrapped query to ensure the untrusted clause has been
774             // fully isolated.
775 
776             // Validate the unwrapped query
777             db.validateSql(unwrappedSql, null); // will throw if query is invalid
778 
779             // Execute wrapped query for extra protection
780             final String wrappedSql = buildDelete(wrap(selection));
781             sql = wrappedSql;
782         } else {
783             // Execute unwrapped query
784             sql = unwrappedSql;
785         }
786 
787         final String[] sqlArgs = selectionArgs;
788         if (Log.isLoggable(TAG, Log.DEBUG)) {
789             if (Logging.IS_DEBUGGABLE) {
790                 Log.d(TAG, sql + " with args " + Arrays.toString(sqlArgs));
791             } else {
792                 Log.d(TAG, sql);
793             }
794         }
795         return com.android.providers.media.util.DatabaseUtils.executeUpdateDelete(db, sql, sqlArgs);
796     }
797 
798     private static boolean hasUserWhere(@Nullable String selection) {
799         return !Strings.isNullOrEmpty(selection);
800     }
801 
802     private void enforceStrictColumns(@Nullable String[] projection) {
803         Objects.requireNonNull(mProjectionMap, "No projection map defined");
804         if (!isStrictColumns()) {
805             return;
806         }
807 
808         computeProjection(projection);
809     }
810 
811     private void enforceStrictColumns(@NonNull ContentValues values) {
812         Objects.requireNonNull(mProjectionMap, "No projection map defined");
813         if (!isStrictColumns()) {
814             return;
815         }
816 
817         final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
818                 .getValues(values);
819         for (int i = 0; i < rawValues.size(); i++) {
820             final String column = rawValues.keyAt(i);
821             if (!mProjectionMap.containsKey(column)) {
822                 throw new IllegalArgumentException("Invalid column " + column);
823             }
824         }
825     }
826 
827     @VisibleForTesting
828     void enforceStrictGrammar(@Nullable String selection, @Nullable String groupBy,
829             @Nullable String having, @Nullable String sortOrder, @Nullable String limit) {
830         SQLiteTokenizer.tokenize(selection, SQLiteTokenizer.OPTION_NONE,
831                 this::enforceStrictToken);
832         SQLiteTokenizer.tokenize(groupBy, SQLiteTokenizer.OPTION_NONE,
833                 this::enforceStrictToken);
834         SQLiteTokenizer.tokenize(having, SQLiteTokenizer.OPTION_NONE,
835                 this::enforceStrictToken);
836         SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE,
837                 this::enforceStrictToken);
838         SQLiteTokenizer.tokenize(limit, SQLiteTokenizer.OPTION_NONE,
839                 this::enforceStrictToken);
840     }
841 
842     private void enforceStrictToken(@NonNull String token) {
843         if (TextUtils.isEmpty(token)) return;
844         if (isTableOrColumn(token)) return;
845         if (isCustomCollator(token)) return;
846         if (SQLiteTokenizer.isFunction(token)) return;
847         if (SQLiteTokenizer.isType(token)) return;
848 
849         // Carefully block any tokens that are attempting to jump across query
850         // clauses or create subqueries, since they could leak data that should
851         // have been filtered by the trusted where clause
852         boolean isAllowedKeyword = SQLiteTokenizer.isKeyword(token);
853         switch (token.toUpperCase(Locale.ROOT)) {
854             case "SELECT":
855             case "FROM":
856             case "WHERE":
857             case "GROUP":
858             case "HAVING":
859             case "WINDOW":
860             case "VALUES":
861             case "ORDER":
862             case "LIMIT":
863                 isAllowedKeyword = false;
864                 break;
865         }
866         if (isAllowedKeyword) return;
867 
868         if (mTargetSdkVersion < Build.VERSION_CODES.R) {
869             // Narrow concessions to support legacy apps that aren't using
870             // proper SQL string substitution
871             if (sPattern154193772.matcher(token).matches()) return;
872             if (sPattern156832140.matcher(token).matches()) return;
873             if (sPattern158537159.matcher(token).matches()) return;
874         }
875 
876         throw new IllegalArgumentException("Invalid token " + token);
877     }
878 
879     /**
880      * Construct a {@code SELECT} statement suitable for use in a group of
881      * {@code SELECT} statements that will be joined through {@code UNION} operators
882      * in buildUnionQuery.
883      *
884      * @param projectionIn A list of which columns to return. Passing
885      *    null will return all columns, which is discouraged to
886      *    prevent reading data from storage that isn't going to be
887      *    used.
888      * @param selection A filter declaring which rows to return,
889      *   formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
890      *   itself).  Passing null will return all rows for the given
891      *   URL.
892      * @param groupBy A filter declaring how to group rows, formatted
893      *   as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY} itself).
894      *   Passing null will cause the rows to not be grouped.
895      * @param having A filter declare which row groups to include in
896      *   the cursor, if row grouping is being used, formatted as an
897      *   SQL {@code HAVING} clause (excluding the {@code HAVING} itself).  Passing
898      *   null will cause all row groups to be included, and is
899      *   required when row grouping is not being used.
900      * @param sortOrder How to order the rows, formatted as an SQL
901      *   {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
902      *   will use the default sort order, which may be unordered.
903      * @param limit Limits the number of rows returned by the query,
904      *   formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
905      * @return the resulting SQL {@code SELECT} statement
906      */
907     public String buildQuery(
908             String[] projectionIn, String selection, String groupBy,
909             String having, String sortOrder, String limit) {
910         String[] projection = computeProjection(projectionIn);
911         String where = computeWhere(selection);
912 
913         return buildQueryString(
914                 mDistinct, mTables, projection, where,
915                 groupBy, having, sortOrder, limit);
916     }
917 
918     /** {@hide} */
919     public String buildInsert(ContentValues values) {
920         if (values == null || values.isEmpty()) {
921             throw new IllegalArgumentException("Empty values");
922         }
923 
924         StringBuilder sql = new StringBuilder(120);
925         sql.append("INSERT INTO ");
926         sql.append(SQLiteDatabase.findEditTable(mTables));
927         sql.append(" (");
928 
929         final boolean hasGeneration = Objects.equals(mTables, "files");
930         if (hasGeneration) {
931             values.remove(MediaColumns.GENERATION_ADDED);
932             values.remove(MediaColumns.GENERATION_MODIFIED);
933         }
934 
935         final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
936                 .getValues(values);
937         for (int i = 0; i < rawValues.size(); i++) {
938             if (i > 0) {
939                 sql.append(',');
940             }
941             sql.append(rawValues.keyAt(i));
942         }
943         if (hasGeneration) {
944             sql.append(',');
945             sql.append(MediaColumns.GENERATION_ADDED);
946             sql.append(',');
947             sql.append(MediaColumns.GENERATION_MODIFIED);
948         }
949         if (shouldAppendRowId(values)) {
950             sql.append(',');
951             sql.append(MediaColumns._ID);
952         }
953 
954         sql.append(") VALUES (");
955         for (int i = 0; i < rawValues.size(); i++) {
956             if (i > 0) {
957                 sql.append(',');
958             }
959             sql.append('?');
960         }
961         if (hasGeneration) {
962             sql.append(',');
963             sql.append('(');
964             sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
965             sql.append(')');
966             sql.append(',');
967             sql.append('(');
968             sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
969             sql.append(')');
970         }
971         if (shouldAppendRowId(values)) {
972             sql.append(',');
973             sql.append(bindSelection(GET_ID_FOR_INSERT_CLAUSE,
974                     values.getAsString(MediaColumns.DATA)));
975         }
976         sql.append(")");
977         return sql.toString();
978     }
979 
980     /** {@hide} */
981     public String buildUpdate(ContentValues values, String selection) {
982         if (values == null || values.isEmpty()) {
983             throw new IllegalArgumentException("Empty values");
984         }
985 
986         StringBuilder sql = new StringBuilder(120);
987         sql.append("UPDATE ");
988         sql.append(SQLiteDatabase.findEditTable(mTables));
989         sql.append(" SET ");
990 
991         final boolean hasGeneration = Objects.equals(mTables, "files");
992         if (hasGeneration) {
993             values.remove(MediaColumns.GENERATION_ADDED);
994             values.remove(MediaColumns.GENERATION_MODIFIED);
995         }
996 
997         final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
998                 .getValues(values);
999         for (int i = 0; i < rawValues.size(); i++) {
1000             if (i > 0) {
1001                 sql.append(',');
1002             }
1003             sql.append(rawValues.keyAt(i));
1004             sql.append("=?");
1005         }
1006         if (hasGeneration) {
1007             sql.append(',');
1008             sql.append(MediaColumns.GENERATION_MODIFIED);
1009             sql.append('=');
1010             sql.append('(');
1011             sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
1012             sql.append(')');
1013         }
1014         if (shouldAppendRowId(values)) {
1015             sql.append(',');
1016             sql.append(MediaColumns._ID);
1017             sql.append('=');
1018             sql.append(bindSelection(GET_ID_FOR_UPDATE_CLAUSE,
1019                     values.getAsString(MediaColumns.DATA)));
1020         }
1021 
1022         final String where = computeWhere(selection);
1023         appendClause(sql, " WHERE ", where);
1024         return sql.toString();
1025     }
1026 
1027     /** {@hide} */
1028     public String buildDelete(String selection) {
1029         StringBuilder sql = new StringBuilder(120);
1030         sql.append("DELETE FROM ");
1031         sql.append(SQLiteDatabase.findEditTable(mTables));
1032 
1033         final String where = computeWhere(selection);
1034         appendClause(sql, " WHERE ", where);
1035         return sql.toString();
1036     }
1037 
1038     private static @NonNull String maybeWithOperator(@Nullable String operator,
1039             @NonNull String column) {
1040         if (operator != null) {
1041             return operator + "(" + column + ")";
1042         } else {
1043             return column;
1044         }
1045     }
1046 
1047     /** {@hide} */
1048     public @Nullable String[] computeProjection(@Nullable String[] projectionIn) {
1049         if (projectionIn != null && projectionIn.length > 0) {
1050             String[] projectionOut = new String[projectionIn.length];
1051             for (int i = 0; i < projectionIn.length; i++) {
1052                 projectionOut[i] = computeSingleProjectionOrThrow(projectionIn[i]);
1053             }
1054             return projectionOut;
1055         } else {
1056             return getAllColumnsFromProjectionMap();
1057         }
1058     }
1059 
1060     /** {@hide} */
1061     public String[] getAllColumnsFromProjectionMap() {
1062         if (mProjectionMap != null) {
1063             Set<Entry<String, String>> entrySet = mProjectionMap.entrySet();
1064             String[] projection = new String[entrySet.size()];
1065             Iterator<Entry<String, String>> entryIter = entrySet.iterator();
1066             int i = 0;
1067 
1068             while (entryIter.hasNext()) {
1069                 Entry<String, String> entry = entryIter.next();
1070 
1071                 // Don't include the _count column when people ask for no projection.
1072                 if (entry.getKey().equals(BaseColumns._COUNT)) {
1073                     continue;
1074                 }
1075                 projection[i++] = entry.getValue();
1076             }
1077             return projection;
1078         }
1079         return null;
1080     }
1081 
1082     private @NonNull String computeSingleProjectionOrThrow(@NonNull String userColumn) {
1083         final String column = computeSingleProjection(userColumn);
1084         if (column != null) {
1085             return column;
1086         } else {
1087             if (isStrictColumns()) {
1088                 throw new IllegalArgumentException("Invalid column " + userColumn);
1089             }
1090             return userColumn;
1091         }
1092     }
1093 
1094     private @Nullable String computeSingleProjection(@NonNull String userColumn) {
1095         // When no mapping provided, anything goes
1096         if (mProjectionMap == null) {
1097             return userColumn;
1098         }
1099 
1100         String operator = null;
1101         String column = mProjectionMap.get(userColumn.toLowerCase(Locale.ROOT));
1102 
1103         // When no direct match found, look for aggregation
1104         if (column == null) {
1105             final Matcher matcher = sAggregationPattern.matcher(userColumn);
1106             if (matcher.matches()) {
1107                 operator = matcher.group(1);
1108                 userColumn = matcher.group(2);
1109                 column = mProjectionMap.get(userColumn.toLowerCase(Locale.ROOT));
1110             }
1111         }
1112 
1113         if (column != null) {
1114             return maybeWithOperator(operator, column);
1115         }
1116 
1117         if (mStrictFlags == 0 &&
1118                 (userColumn.contains(" AS ") || userColumn.contains(" as "))) {
1119             /* A column alias already exist */
1120             return maybeWithOperator(operator, userColumn);
1121         }
1122 
1123         // If allowlist is configured, we might be willing to let
1124         // this custom column bypass our strict checks.
1125         if (mProjectionAllowlist != null) {
1126             boolean match = false;
1127             for (Pattern p : mProjectionAllowlist) {
1128                 if (p.matcher(userColumn).matches()) {
1129                     match = true;
1130                     break;
1131                 }
1132             }
1133 
1134             if (match) {
1135                 Log.w(TAG, "Allowing abusive custom column: " + userColumn);
1136                 return maybeWithOperator(operator, userColumn);
1137             }
1138         }
1139 
1140         return null;
1141     }
1142 
1143     private boolean isTableOrColumn(String token) {
1144         if (mTables.equals(token)) return true;
1145         return computeSingleProjection(token) != null;
1146     }
1147 
1148     private boolean isCustomCollator(String token) {
1149         return sCustomCollatorPattern.matcher(token).matches();
1150     }
1151 
1152     /** {@hide} */
1153     public @Nullable String computeWhere(@Nullable String selection) {
1154         final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
1155         final boolean hasExternal = !TextUtils.isEmpty(selection);
1156 
1157         if (hasInternal || hasExternal) {
1158             final StringBuilder where = new StringBuilder();
1159             if (hasInternal) {
1160                 where.append('(').append(mWhereClause).append(')');
1161             }
1162             if (hasInternal && hasExternal) {
1163                 where.append(" AND ");
1164             }
1165             if (hasExternal) {
1166                 where.append('(').append(selection).append(')');
1167             }
1168             return where.toString();
1169         } else {
1170             return null;
1171         }
1172     }
1173 
1174     /**
1175      * Wrap given argument in parenthesis, unless it's {@code null} or
1176      * {@code ()}, in which case return it verbatim.
1177      */
1178     private @Nullable String wrap(@Nullable String arg) {
1179         if (TextUtils.isEmpty(arg)) {
1180             return arg;
1181         } else {
1182             return "(" + arg + ")";
1183         }
1184     }
1185 
1186     @VisibleForTesting
1187     boolean shouldAppendRowId(ContentValues values) {
1188         // When no projectionMap provided, don't add the row
1189         final boolean hasIdInProjectionMap = mProjectionMap != null && mProjectionMap.containsKey(
1190                 MediaColumns._ID) && TextUtils.equals(mProjectionMap.get(MediaColumns._ID),
1191                 MediaColumns._ID);
1192         return !values.containsKey(MediaColumns._ID) && values.containsKey(MediaColumns.DATA)
1193                 && hasIdInProjectionMap;
1194     }
1195 }
1196