blob: b707f6107d7716d8b5769182a6ed42f387b47109 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.webrestrictions.browser;
import android.annotation.TargetApi;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.content.pm.ProviderInfo;
import android.database.AbstractCursor;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Abstract content provider for providing web restrictions, i.e. for providing a filter for URLs so
* that they can be blocked or permitted, and a means of requesting permission for new URLs. It
* provides two (virtual) tables; an 'authorized' table listing the the status of every URL, and a
* 'requested' table containing the requests for access to new URLs. The 'authorized' table is read
* only and the 'requested' table is write only.
*/
public abstract class WebRestrictionsContentProvider extends ContentProvider {
public static final int BLOCKED = 0;
public static final int PROCEED = 1;
private static final int WEB_RESTRICTIONS = 1;
private static final int AUTHORIZED = 2;
private static final int REQUESTED = 3;
private final Pattern mSelectionPattern;
private UriMatcher mContentUriMatcher;
private Uri mContentUri;
/**
* Structure for returning result including the custom error data.
*/
public static class WebRestrictionsResult {
private final boolean mShouldProceed;
private final int mErrorInts[];
private final String mErrorStrings[];
public WebRestrictionsResult(
boolean shouldProceed, final int[] errorInts, final String[] errorStrings) {
assert !shouldProceed || errorInts == null;
assert !shouldProceed || errorStrings == null;
mShouldProceed = shouldProceed;
mErrorInts = errorInts == null ? null : errorInts.clone();
mErrorStrings = errorStrings == null ? null : errorStrings.clone();
}
public int getErrorInt(int i) {
if (mErrorInts == null || i >= mErrorInts.length) return 0;
return mErrorInts[i];
}
public String getErrorString(int i) {
if (mErrorStrings == null || i >= mErrorStrings.length) return null;
return mErrorStrings[i];
}
public boolean shouldProceed() {
return mShouldProceed;
}
public int errorIntCount() {
if (mErrorInts == null) return 0;
return mErrorInts.length;
}
public int errorStringCount() {
if (mErrorStrings == null) return 0;
return mErrorStrings.length;
}
}
protected WebRestrictionsContentProvider() {
// Pattern to extract the URL from the selection.
// Matches patterns of the form "url = '<url>'" with arbitrary spacing
// around the "=" etc.
mSelectionPattern = Pattern.compile("\\s*url\\s*=\\s*'([^']*)'");
}
@Override
public boolean onCreate() {
return true;
}
@Override
public void attachInfo(Context context, ProviderInfo info) {
super.attachInfo(context, info);
mContentUri = new Uri.Builder().scheme("content").authority(info.authority).build();
mContentUriMatcher = new UriMatcher(WEB_RESTRICTIONS);
mContentUriMatcher.addURI(info.authority, "authorized", AUTHORIZED);
mContentUriMatcher.addURI(info.authority, "requested", REQUESTED);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
if (!contentProviderEnabled()) return null;
// Check that this is the a query on the 'authorized' table
// TODO(aberent): Provide useful queries on the 'requested' table.
if (mContentUriMatcher.match(uri) != AUTHORIZED) return null;
// If the selection is of the right form get the url we are querying.
Matcher matcher = mSelectionPattern.matcher(selection);
if (!matcher.find()) return null;
final String url = matcher.group(1);
final WebRestrictionsResult result = shouldProceed(maybeGetCallingPackage(), url);
if (result == null) return null;
return new AbstractCursor() {
@Override
public int getCount() {
return 1;
}
@Override
public String[] getColumnNames() {
String errorNames[] = getErrorColumnNames();
// The cursor in the client gets the column count from the number of column names
// so it is important to limit this array to the actual number of columns.
String names[] = new String[getColumnCount()];
names[0] = "Should Proceed";
for (int i = 0; i < getColumnCount() - 1; i++) {
names[i + 1] = errorNames[i];
}
return names;
}
@Override
public String getString(int column) {
// The column order is:
// result,
// integer error parameters,
// string error parameters
// so offset the string error parameters by the number of integer parameters + 1
int errorStringNumber = column - result.errorIntCount() - 1;
if (errorStringNumber >= 0 && errorStringNumber < result.errorStringCount()) {
return result.getErrorString(errorStringNumber);
}
return null;
}
@Override
public short getShort(int column) {
return (short) getLong(column);
}
@Override
public int getInt(int column) {
return (int) getLong(column);
}
@Override
public long getLong(int column) {
if (column == 0) return result.shouldProceed() ? PROCEED : BLOCKED;
// The column order is:
// result,
// integer error parameters,
// string error parameters
// so offset the integer error parameters by 1
int errorIntNumber = column - 1;
if (errorIntNumber < result.errorIntCount()) {
return result.getErrorInt(errorIntNumber);
}
return 0;
}
@Override
public float getFloat(int column) {
return 0;
}
@Override
public double getDouble(int column) {
return 0;
}
@Override
public boolean isNull(int column) {
return false;
}
@Override
public int getType(int column) {
if (column < result.errorIntCount() + 1) return FIELD_TYPE_INTEGER;
if (column < result.errorIntCount() + result.errorStringCount() + 1) {
return FIELD_TYPE_STRING;
}
return FIELD_TYPE_NULL;
}
@Override
public int getColumnCount() {
return result.errorIntCount() + result.errorStringCount() + 1;
}
};
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private String maybeGetCallingPackage() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return null;
return getCallingPackage();
}
@Override
public String getType(Uri uri) {
// Abused to return whether we can insert
if (!contentProviderEnabled()) return null;
if (mContentUriMatcher.match(uri) != REQUESTED) return null;
return canInsert() ? "text/plain" : null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
if (!contentProviderEnabled()) return null;
if (mContentUriMatcher.match(uri) != REQUESTED) return null;
String url = values.getAsString("url");
if (requestInsert(url)) {
// TODO(aberent): If we ever make the 'requested' table readable then we might want to
// change this to a more conventional content URI (with a row number).
return uri.buildUpon().appendPath(url).build();
} else {
return null;
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
/**
* @param the package calling the content provider, or null if the package is not available.
* @param url the URL that is wanted.
* @return a pair containing the Result and the HTML Error Message. result is true if safe to
* proceed, false otherwise. error message is only meaningful if result is false, a null
* error message means use application default.
*/
protected abstract WebRestrictionsResult shouldProceed(String callingPackage, String url);
/**
* @return whether the content provider allows insertions.
*/
protected abstract boolean canInsert();
/**
* @return the names of the custom error columns, integer valued columns must proceed string
* valued columns.
*/
protected abstract String[] getErrorColumnNames();
/**
* Start a request that a URL should be permitted
*
* @param url the URL that is wanted.
*/
protected abstract boolean requestInsert(final String url);
/**
* @return true if the content provider is enabled, false if not
*/
protected abstract boolean contentProviderEnabled();
/**
* Call to tell observers that the filter has changed.
*/
protected void onFilterChanged() {
getContext().getContentResolver().notifyChange(
mContentUri.buildUpon().appendPath("authorized").build(), null);
}
}