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