/*
* ownCloud Android client application
*
* @author Bartek Przybylski
* @author David A. Velasco
* @author masensio
* Copyright (C) 2011 Bartek Przybylski
* Copyright (C) 2016 ownCloud Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package com.owncloud.android.providers;
import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Binder;
import android.text.TextUtils;
import com.nextcloud.client.core.Clock;
import com.nextcloud.client.database.NextcloudDatabase;
import com.owncloud.android.R;
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.utils.MimeType;
import java.util.ArrayList;
import java.util.Locale;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.SupportSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteQueryBuilder;
import dagger.android.AndroidInjection;
import third_parties.aosp.SQLiteTokenizer;
/**
* The ContentProvider for the ownCloud App.
*/
@SuppressWarnings("PMD.AvoidDuplicateLiterals")
public class FileContentProvider extends ContentProvider {
private static final int SINGLE_FILE = 1;
private static final int DIRECTORY = 2;
private static final int ROOT_DIRECTORY = 3;
private static final int SHARES = 4;
private static final int CAPABILITIES = 5;
private static final int UPLOADS = 6;
private static final int SYNCED_FOLDERS = 7;
private static final int EXTERNAL_LINKS = 8;
private static final int VIRTUAL = 10;
private static final int FILESYSTEM = 11;
private static final String TAG = FileContentProvider.class.getSimpleName();
// todo avoid string concatenation and use string formatting instead later.
private static final String ERROR = "ERROR ";
private static final int SINGLE_PATH_SEGMENT = 1;
public static final int MINIMUM_PATH_SEGMENTS_SIZE = 1;
private static final String[] PROJECTION_CONTENT_TYPE = new String[]{
ProviderTableMeta._ID, ProviderTableMeta.FILE_CONTENT_TYPE
};
private static final String[] PROJECTION_REMOTE_ID = new String[]{
ProviderTableMeta._ID, ProviderTableMeta.FILE_REMOTE_ID
};
private static final String[] PROJECTION_FILE_PATH_AND_OWNER = new String[]{
ProviderTableMeta._ID, ProviderTableMeta.FILE_PATH, ProviderTableMeta.FILE_ACCOUNT_OWNER
};
@Inject protected Clock clock;
@Inject NextcloudDatabase database;
private SupportSQLiteOpenHelper mDbHelper;
private Context mContext;
private UriMatcher mUriMatcher;
@Override
public int delete(@NonNull Uri uri, String where, String[] whereArgs) {
if (isCallerNotAllowed(uri)) {
return -1;
}
int count;
SupportSQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransaction();
try {
count = delete(db, uri, where, whereArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
mContext.getContentResolver().notifyChange(uri, null);
return count;
}
private int delete(SupportSQLiteDatabase db, Uri uri, String where, String... whereArgs) {
if (isCallerNotAllowed(uri)) {
return -1;
}
// verify where for public paths
switch (mUriMatcher.match(uri)) {
case ROOT_DIRECTORY:
case SINGLE_FILE:
case DIRECTORY:
VerificationUtils.verifyWhere(where);
}
int count;
switch (mUriMatcher.match(uri)) {
case SINGLE_FILE:
count = deleteSingleFile(db, uri, where, whereArgs);
break;
case DIRECTORY:
count = deleteDirectory(db, uri, where, whereArgs);
break;
case ROOT_DIRECTORY:
count = db.delete(ProviderTableMeta.FILE_TABLE_NAME, where, whereArgs);
break;
case SHARES:
count = db.delete(ProviderTableMeta.OCSHARES_TABLE_NAME, where, whereArgs);
break;
case CAPABILITIES:
count = db.delete(ProviderTableMeta.CAPABILITIES_TABLE_NAME, where, whereArgs);
break;
case UPLOADS:
count = db.delete(ProviderTableMeta.UPLOADS_TABLE_NAME, where, whereArgs);
break;
case SYNCED_FOLDERS:
count = db.delete(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, where, whereArgs);
break;
case EXTERNAL_LINKS:
count = db.delete(ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME, where, whereArgs);
break;
case VIRTUAL:
count = db.delete(ProviderTableMeta.VIRTUAL_TABLE_NAME, where, whereArgs);
break;
case FILESYSTEM:
count = db.delete(ProviderTableMeta.FILESYSTEM_TABLE_NAME, where, whereArgs);
break;
default:
throw new IllegalArgumentException(String.format(Locale.US, "Unknown uri: %s", uri.toString()));
}
return count;
}
private int deleteDirectory(SupportSQLiteDatabase db, Uri uri, String where, String... whereArgs) {
int count = 0;
Cursor children = query(uri, PROJECTION_CONTENT_TYPE, null, null, null);
if (children != null) {
if (children.moveToFirst()) {
long childId;
boolean isDir;
while (!children.isAfterLast()) {
childId = children.getLong(children.getColumnIndexOrThrow(ProviderTableMeta._ID));
isDir = MimeType.DIRECTORY.equals(children.getString(
children.getColumnIndexOrThrow(ProviderTableMeta.FILE_CONTENT_TYPE)
));
if (isDir) {
count += delete(db, ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, childId),
null, (String[]) null);
} else {
count += delete(db, ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, childId),
null, (String[]) null);
}
children.moveToNext();
}
}
children.close();
}
if (uri.getPathSegments().size() > MINIMUM_PATH_SEGMENTS_SIZE) {
count += deleteWithUri(db, uri, where, whereArgs);
}
return count;
}
private int deleteSingleFile(SupportSQLiteDatabase db, Uri uri, String where, String... whereArgs) {
int count = 0;
try (Cursor c = query(db, uri, PROJECTION_REMOTE_ID, where, whereArgs, null)) {
if (c.moveToFirst()) {
String id = c.getString(c.getColumnIndexOrThrow(ProviderTableMeta._ID));
Log_OC.d(TAG, "Removing FILE " + id);
}
count = deleteWithUri(db, uri, where, whereArgs);
} catch (Exception e) {
Log_OC.d(TAG, "DB-Error removing file!", e);
}
return count;
}
private int deleteWithUri(SupportSQLiteDatabase db, Uri uri, String where, String[] whereArgs) {
final String[] argsWithUri = VerificationUtils.prependUriFirstSegmentToSelectionArgs(whereArgs, uri);
return db.delete(ProviderTableMeta.FILE_TABLE_NAME,
ProviderTableMeta._ID + "=?"
+ (!TextUtils.isEmpty(where) ? " AND (" + where + ")" : ""), argsWithUri);
}
@Override
public String getType(@NonNull Uri uri) {
switch (mUriMatcher.match(uri)) {
case ROOT_DIRECTORY:
return ProviderTableMeta.CONTENT_TYPE;
case SINGLE_FILE:
return ProviderTableMeta.CONTENT_TYPE_ITEM;
default:
throw new IllegalArgumentException(String.format(Locale.US, "Unknown Uri id: %s", uri));
}
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
if (isCallerNotAllowed(uri)) {
return null;
}
Uri newUri;
SupportSQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransaction();
try {
newUri = insert(db, uri, values);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
mContext.getContentResolver().notifyChange(newUri, null);
return newUri;
}
private Uri insert(SupportSQLiteDatabase db, Uri uri, ContentValues values) {
// verify only for those requests that are not internal (files table)
switch (mUriMatcher.match(uri)) {
case ROOT_DIRECTORY:
case SINGLE_FILE:
case DIRECTORY:
VerificationUtils.verifyColumns(values);
break;
}
switch (mUriMatcher.match(uri)) {
case ROOT_DIRECTORY:
case SINGLE_FILE:
String where = ProviderTableMeta.FILE_PATH + "=? AND " + ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?";
String remotePath = values.getAsString(ProviderTableMeta.FILE_PATH);
String accountName = values.getAsString(ProviderTableMeta.FILE_ACCOUNT_OWNER);
String[] whereArgs = {remotePath, accountName};
Cursor doubleCheck = query(db, uri, PROJECTION_FILE_PATH_AND_OWNER, where, whereArgs, null);
// ugly patch; serious refactoring is needed to reduce work in
// FileDataStorageManager and bring it to FileContentProvider
if (!doubleCheck.moveToFirst()) {
doubleCheck.close();
long rowId = db.insert(ProviderTableMeta.FILE_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values);
if (rowId > 0) {
return ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, rowId);
} else {
throw new SQLException(ERROR + uri);
}
} else {
// file is already inserted; race condition, let's avoid a duplicated entry
Uri insertedFileUri = ContentUris.withAppendedId(
ProviderTableMeta.CONTENT_URI_FILE,
doubleCheck.getLong(doubleCheck.getColumnIndexOrThrow(ProviderTableMeta._ID))
);
doubleCheck.close();
return insertedFileUri;
}
case SHARES:
Uri insertedShareUri;
long idShares = db.insert(ProviderTableMeta.OCSHARES_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values);
if (idShares > 0) {
insertedShareUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_SHARE, idShares);
} else {
throw new SQLException(ERROR + uri);
}
updateFilesTableAccordingToShareInsertion(db, values);
return insertedShareUri;
case CAPABILITIES:
Uri insertedCapUri;
long idCapabilities = db.insert(ProviderTableMeta.CAPABILITIES_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values);
if (idCapabilities > 0) {
insertedCapUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_CAPABILITIES, idCapabilities);
} else {
throw new SQLException(ERROR + uri);
}
return insertedCapUri;
case UPLOADS:
Uri insertedUploadUri;
long uploadId = db.insert(ProviderTableMeta.UPLOADS_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values);
if (uploadId > 0) {
insertedUploadUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_UPLOADS, uploadId);
} else {
throw new SQLException(ERROR + uri);
}
return insertedUploadUri;
case SYNCED_FOLDERS:
Uri insertedSyncedFolderUri;
long syncedFolderId = db.insert(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values);
if (syncedFolderId > 0) {
insertedSyncedFolderUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_SYNCED_FOLDERS,
syncedFolderId);
} else {
throw new SQLException("ERROR " + uri);
}
return insertedSyncedFolderUri;
case EXTERNAL_LINKS:
Uri insertedExternalLinkUri;
long externalLinkId = db.insert(ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values);
if (externalLinkId > 0) {
insertedExternalLinkUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_EXTERNAL_LINKS,
externalLinkId);
} else {
throw new SQLException("ERROR " + uri);
}
return insertedExternalLinkUri;
case VIRTUAL:
Uri insertedVirtualUri;
long virtualId = db.insert(ProviderTableMeta.VIRTUAL_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values);
if (virtualId > 0) {
insertedVirtualUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_VIRTUAL, virtualId);
} else {
throw new SQLException("ERROR " + uri);
}
return insertedVirtualUri;
case FILESYSTEM:
Uri insertedFilesystemUri;
long filesystemId = db.insert(ProviderTableMeta.FILESYSTEM_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values);
if (filesystemId > 0) {
insertedFilesystemUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILESYSTEM,
filesystemId);
} else {
throw new SQLException("ERROR " + uri);
}
return insertedFilesystemUri;
default:
throw new IllegalArgumentException("Unknown uri id: " + uri);
}
}
private void updateFilesTableAccordingToShareInsertion(SupportSQLiteDatabase db, ContentValues newShare) {
ContentValues fileValues = new ContentValues();
ShareType newShareType = ShareType.fromValue(newShare.getAsInteger(ProviderTableMeta.OCSHARES_SHARE_TYPE));
switch (newShareType) {
case PUBLIC_LINK:
fileValues.put(ProviderTableMeta.FILE_SHARED_VIA_LINK, 1);
break;
case USER:
case GROUP:
case EMAIL:
case FEDERATED:
case FEDERATED_GROUP:
case ROOM:
case CIRCLE:
case DECK:
case GUEST:
fileValues.put(ProviderTableMeta.FILE_SHARED_WITH_SHAREE, 1);
break;
default:
// everything should be handled
}
String where = ProviderTableMeta.FILE_PATH + "=? AND " + ProviderTableMeta.FILE_ACCOUNT_OWNER + "=?";
String[] whereArgs = new String[]{
newShare.getAsString(ProviderTableMeta.OCSHARES_PATH),
newShare.getAsString(ProviderTableMeta.OCSHARES_ACCOUNT_OWNER)
};
db.update(ProviderTableMeta.FILE_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, fileValues, where, whereArgs);
}
@Override
public boolean onCreate() {
AndroidInjection.inject(this);
mDbHelper = database.getOpenHelper();
mContext = getContext();
if (mContext == null) {
return false;
}
String authority = mContext.getResources().getString(R.string.authority);
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(authority, null, ROOT_DIRECTORY);
mUriMatcher.addURI(authority, "file/", SINGLE_FILE);
mUriMatcher.addURI(authority, "file/#", SINGLE_FILE);
mUriMatcher.addURI(authority, "dir/", DIRECTORY);
mUriMatcher.addURI(authority, "dir/#", DIRECTORY);
mUriMatcher.addURI(authority, "shares/", SHARES);
mUriMatcher.addURI(authority, "shares/#", SHARES);
mUriMatcher.addURI(authority, "capabilities/", CAPABILITIES);
mUriMatcher.addURI(authority, "capabilities/#", CAPABILITIES);
mUriMatcher.addURI(authority, "uploads/", UPLOADS);
mUriMatcher.addURI(authority, "uploads/#", UPLOADS);
mUriMatcher.addURI(authority, "synced_folders", SYNCED_FOLDERS);
mUriMatcher.addURI(authority, "external_links", EXTERNAL_LINKS);
mUriMatcher.addURI(authority, "virtual", VIRTUAL);
mUriMatcher.addURI(authority, "filesystem", FILESYSTEM);
return true;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
// skip check for files as they need to be queried to get access via document provider
switch (mUriMatcher.match(uri)) {
case ROOT_DIRECTORY:
case SINGLE_FILE:
case DIRECTORY:
break;
default:
if (isCallerNotAllowed(uri)) {
return null;
}
}
Cursor result;
SupportSQLiteDatabase db = mDbHelper.getReadableDatabase();
db.beginTransaction();
try {
result = query(db, uri, projection, selection, selectionArgs, sortOrder);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return result;
}
private Cursor query(SupportSQLiteDatabase db,
Uri uri,
String[] projectionArray,
String selection,
String[] selectionArgs,
String sortOrder) {
// verify only for those requests that are not internal
final int uriMatch = mUriMatcher.match(uri);
String tableName;
switch (uriMatch) {
case ROOT_DIRECTORY:
case DIRECTORY:
case SINGLE_FILE:
VerificationUtils.verifyWhere(selection); // prevent injection in public paths
tableName = ProviderTableMeta.FILE_TABLE_NAME;
break;
case SHARES:
tableName = ProviderTableMeta.OCSHARES_TABLE_NAME;
break;
case CAPABILITIES:
tableName = ProviderTableMeta.CAPABILITIES_TABLE_NAME;
break;
case UPLOADS:
tableName = ProviderTableMeta.UPLOADS_TABLE_NAME;
break;
case SYNCED_FOLDERS:
tableName = ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME;
break;
case EXTERNAL_LINKS:
tableName = ProviderTableMeta.EXTERNAL_LINKS_TABLE_NAME;
break;
case VIRTUAL:
tableName = ProviderTableMeta.VIRTUAL_TABLE_NAME;
break;
case FILESYSTEM:
tableName = ProviderTableMeta.FILESYSTEM_TABLE_NAME;
break;
default:
throw new IllegalArgumentException("Unknown uri id: " + uri);
}
SupportSQLiteQueryBuilder queryBuilder = SupportSQLiteQueryBuilder.builder(tableName);
// add ID to arguments if Uri has more than one segment
if (uriMatch != ROOT_DIRECTORY && uri.getPathSegments().size() > SINGLE_PATH_SEGMENT) {
String idColumn = uriMatch == DIRECTORY ? ProviderTableMeta.FILE_PARENT : ProviderTableMeta._ID;
selection = idColumn + "=? AND " + selection;
selectionArgs = VerificationUtils.prependUriFirstSegmentToSelectionArgs(selectionArgs, uri);
}
String order;
if (TextUtils.isEmpty(sortOrder)) {
switch (uriMatch) {
case SHARES:
order = ProviderTableMeta.OCSHARES_DEFAULT_SORT_ORDER;
break;
case CAPABILITIES:
order = ProviderTableMeta.CAPABILITIES_DEFAULT_SORT_ORDER;
break;
case UPLOADS:
order = ProviderTableMeta.UPLOADS_DEFAULT_SORT_ORDER;
break;
case SYNCED_FOLDERS:
order = ProviderTableMeta.SYNCED_FOLDER_LOCAL_PATH;
break;
case EXTERNAL_LINKS:
order = ProviderTableMeta.EXTERNAL_LINKS_NAME;
break;
case VIRTUAL:
order = ProviderTableMeta.VIRTUAL_TYPE;
break;
default: // Files
order = ProviderTableMeta.FILE_DEFAULT_SORT_ORDER;
break;
case FILESYSTEM:
order = ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH;
break;
}
} else {
if (uriMatch == ROOT_DIRECTORY || uriMatch == SINGLE_FILE || uriMatch == DIRECTORY) {
VerificationUtils.verifySortOrder(sortOrder);
}
order = sortOrder;
}
// DB case_sensitive
db.execSQL("PRAGMA case_sensitive_like = true");
// only file list is publicly accessible via content provider, so only this has to be protected
if ((uriMatch == ROOT_DIRECTORY || uriMatch == SINGLE_FILE ||
uriMatch == DIRECTORY) && projectionArray != null && projectionArray.length > 0) {
for (String column : projectionArray) {
VerificationUtils.verifyColumnName(column);
}
}
// if both are null, let them pass to query
if (selectionArgs == null && selection != null) {
selectionArgs = new String[]{selection};
selection = "(?)";
}
if (!TextUtils.isEmpty(selection)) {
queryBuilder.selection(selection, selectionArgs);
}
if (!TextUtils.isEmpty(order)) {
queryBuilder.orderBy(order);
}
if (projectionArray != null && projectionArray.length > 0) {
queryBuilder.columns(projectionArray);
}
final SupportSQLiteQuery supportSQLiteQuery = queryBuilder.create();
final Cursor c = db.query(supportSQLiteQuery);
c.setNotificationUri(mContext.getContentResolver(), uri);
return c;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
if (isCallerNotAllowed(uri)) {
return -1;
}
int count;
SupportSQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransaction();
try {
count = update(db, uri, values, selection, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
mContext.getContentResolver().notifyChange(uri, null);
return count;
}
private int update(SupportSQLiteDatabase db, Uri uri, ContentValues values, String selection, String... selectionArgs) {
// verify contentValues and selection for public paths to prevent injection
switch (mUriMatcher.match(uri)) {
case ROOT_DIRECTORY:
case SINGLE_FILE:
case DIRECTORY:
VerificationUtils.verifyColumns(values);
VerificationUtils.verifyWhere(selection);
}
switch (mUriMatcher.match(uri)) {
case DIRECTORY:
return 0;
case SHARES:
return db.update(ProviderTableMeta.OCSHARES_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs);
case CAPABILITIES:
return db.update(ProviderTableMeta.CAPABILITIES_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs);
case UPLOADS:
return db.update(ProviderTableMeta.UPLOADS_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs);
case SYNCED_FOLDERS:
return db.update(ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs);
case FILESYSTEM:
return db.update(ProviderTableMeta.FILESYSTEM_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs);
default:
return db.update(ProviderTableMeta.FILE_TABLE_NAME, SQLiteDatabase.CONFLICT_REPLACE, values, selection, selectionArgs);
}
}
@NonNull
@Override
public ContentProviderResult[] applyBatch(@NonNull ArrayList operations)
throws OperationApplicationException {
Log_OC.d("FileContentProvider", "applying batch in provider " + this +
" (temporary: " + isTemporary() + ")");
ContentProviderResult[] results = new ContentProviderResult[operations.size()];
int i = 0;
SupportSQLiteDatabase database = mDbHelper.getWritableDatabase();
database.beginTransaction(); // it's supposed that transactions can be nested
try {
for (ContentProviderOperation operation : operations) {
results[i] = operation.apply(this, results, i);
i++;
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
Log_OC.d("FileContentProvider", "applied batch in provider " + this);
return results;
}
private boolean isCallerNotAllowed(Uri uri) {
switch (mUriMatcher.match(uri)) {
case SHARES:
case CAPABILITIES:
case UPLOADS:
case SYNCED_FOLDERS:
case EXTERNAL_LINKS:
case VIRTUAL:
case FILESYSTEM:
String callingPackage = mContext.getPackageManager().getNameForUid(Binder.getCallingUid());
return callingPackage == null || !callingPackage.equals(mContext.getPackageName());
case ROOT_DIRECTORY:
case SINGLE_FILE:
case DIRECTORY:
default:
return false;
}
}
static class VerificationUtils {
private static boolean isValidColumnName(@NonNull String columnName) {
return ProviderTableMeta.FILE_ALL_COLUMNS.contains(columnName);
}
@VisibleForTesting
public static void verifyColumns(@Nullable ContentValues contentValues) {
if (contentValues == null || contentValues.keySet().isEmpty()) {
return;
}
for (String name : contentValues.keySet()) {
verifyColumnName(name);
}
}
public static void verifyColumnName(@NonNull String columnName) {
if (!isValidColumnName(columnName)) {
throw new IllegalArgumentException(String.format("Column name \"%s\" is not allowed", columnName));
}
}
public static String[] prependUriFirstSegmentToSelectionArgs(@Nullable final String[] originalArgs, final Uri uri) {
String[] args;
if (originalArgs == null) {
args = new String[1];
} else {
args = new String[originalArgs.length + 1];
System.arraycopy(originalArgs, 0, args, 1, originalArgs.length);
}
args[0] = uri.getPathSegments().get(1);
return args;
}
public static void verifySortOrder(@Nullable String sortOrder) {
if (sortOrder == null) {
return;
}
SQLiteTokenizer.tokenize(sortOrder, SQLiteTokenizer.OPTION_NONE, VerificationUtils::verifySortToken);
}
private static void verifySortToken(String token){
// accept empty tokens and valid column names
if (TextUtils.isEmpty(token) || isValidColumnName(token)) {
return;
}
// accept only a small subset of keywords
if(SQLiteTokenizer.isKeyword(token)){
switch (token.toUpperCase(Locale.ROOT)) {
case "ASC":
case "DESC":
case "COLLATE":
case "NOCASE":
return;
}
}
// if none of the above, invalid token
throw new IllegalArgumentException("Invalid token " + token);
}
public static void verifyWhere(@Nullable String where) {
if (where == null) {
return;
}
SQLiteTokenizer.tokenize(where, SQLiteTokenizer.OPTION_NONE, VerificationUtils::verifyWhereToken);
}
private static void verifyWhereToken(String token) {
// allow empty, valid column names, functions (min,max,count) and types
if (TextUtils.isEmpty(token) || isValidColumnName(token)
|| SQLiteTokenizer.isFunction(token) || SQLiteTokenizer.isType(token)) {
return;
}
// Disallow dangerous keywords, allow others
if (SQLiteTokenizer.isKeyword(token)) {
switch (token.toUpperCase(Locale.ROOT)) {
case "SELECT":
case "FROM":
case "WHERE":
case "GROUP":
case "HAVING":
case "WINDOW":
case "VALUES":
case "ORDER":
case "LIMIT":
throw new IllegalArgumentException("Invalid token " + token);
default:
return;
}
}
// if none of the above: invalid token
throw new IllegalArgumentException("Invalid token " + token);
}
}
}