[CB-3384] Rewrite of DataResource into UriResolver + UriResolvers

Includes unit tests woot!

Note that this remove CordovaPlugin.shouldInterceptRequest(). Should be
fine since this method was introduced only a couple of releases ago, was
never documented, and afaict was only used by the Chrome Cordova plugins.
This commit is contained in:
Andrew Grieve
2013-06-07 10:17:28 -04:00
parent fbf7f1c3f9
commit 892ffc8ce4
9 changed files with 694 additions and 77 deletions
@@ -943,4 +943,38 @@ public class CordovaWebView extends WebView {
public void storeResult(int requestCode, int resultCode, Intent intent) {
mResult = new ActivityResult(requestCode, resultCode, intent);
}
/**
* Resolves the given URI, giving plugins a chance to re-route or customly handle the URI.
* A white-list rejection will be returned if the URI does not pass the white-list.
* @return Never returns null.
* @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be
* resolved before being passed into this function.
*/
public UriResolver resolveUri(Uri uri) {
return resolveUri(uri, false);
}
UriResolver resolveUri(Uri uri, boolean fromWebView) {
if (!uri.isAbsolute()) {
throw new IllegalArgumentException("Relative URIs are not yet supported by resolveUri.");
}
// Check the against the white-list before delegating to plugins.
if (("http".equals(uri.getScheme()) || "https".equals(uri.getScheme())) && !Config.isUrlWhiteListed(uri.toString()))
{
LOG.w(TAG, "resolveUri - URL is not in whitelist: " + uri);
return new UriResolvers.ErrorUriResolver(uri, "Whitelist rejection");
}
// Give plugins a chance to handle the request.
UriResolver resolver = pluginManager.resolveUri(uri);
if (resolver == null && !fromWebView) {
resolver = UriResolvers.forUri(uri, cordova.getActivity());
if (resolver == null) {
resolver = new UriResolvers.ErrorUriResolver(uri, "Unresolvable URI");
}
}
return resolver;
}
}
@@ -26,9 +26,12 @@ import org.apache.cordova.api.CordovaInterface;
import org.apache.cordova.api.LOG;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Locale;
public class FileHelper {
@@ -124,6 +127,20 @@ public class FileHelper {
return uriString;
}
public static String getMimeTypeForExtension(String path) {
String extension = path;
int lastDot = extension.lastIndexOf('.');
if (lastDot != -1) {
extension = extension.substring(lastDot + 1);
}
// Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
extension = extension.toLowerCase(Locale.getDefault());
if (extension.equals("3ga")) {
return "audio/3gpp";
}
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
/**
* Returns the mime type of the data specified by the given URI string.
*
@@ -137,19 +154,7 @@ public class FileHelper {
if (uriString.startsWith("content://")) {
mimeType = cordova.getActivity().getContentResolver().getType(uri);
} else {
// MimeTypeMap.getFileExtensionFromUrl() fails when there are query parameters.
String extension = uri.getPath();
int lastDot = extension.lastIndexOf('.');
if (lastDot != -1) {
extension = extension.substring(lastDot + 1);
}
// Convert the URI string to lower case to ensure compatibility with MimeTypeMap (see CB-2185).
extension = extension.toLowerCase();
if (extension.equals("3ga")) {
mimeType = "audio/3gpp";
} else {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
mimeType = getMimeTypeForExtension(uri.getPath());
}
return mimeType;
@@ -18,7 +18,6 @@
*/
package org.apache.cordova;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -26,6 +25,7 @@ import org.apache.cordova.api.CordovaInterface;
import org.apache.cordova.api.LOG;
import android.annotation.TargetApi;
import android.net.Uri;
import android.os.Build;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
@@ -44,45 +44,29 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient {
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
//Check if plugins intercept the request
WebResourceResponse ret = super.shouldInterceptRequest(view, url);
UriResolver uriResolver = appView.resolveUri(Uri.parse(url), true);
if(!Config.isUrlWhiteListed(url) && (url.startsWith("http://") || url.startsWith("https://")))
{
ret = getWhitelistResponse();
if (uriResolver == null && url.startsWith("file:///android_asset/")) {
if (url.contains("?") || url.contains("#") || needsIceCreamSpecialsInAssetUrlFix(url)) {
uriResolver = appView.resolveUri(Uri.parse(url), false);
}
}
else if(ret == null && (url.contains("?") || url.contains("#") || needsIceCreamSpecialsInAssetUrlFix(url))){
ret = generateWebResourceResponse(url);
}
else if (ret == null && this.appView.pluginManager != null) {
ret = this.appView.pluginManager.shouldInterceptRequest(url);
}
return ret;
}
private WebResourceResponse getWhitelistResponse()
{
WebResourceResponse emptyResponse;
String empty = "";
ByteArrayInputStream data = new ByteArrayInputStream(empty.getBytes());
return new WebResourceResponse("text/plain", "UTF-8", data);
}
private WebResourceResponse generateWebResourceResponse(String url) {
if (url.startsWith("file:///android_asset/")) {
String mimetype = FileHelper.getMimeType(url, cordova);
if (uriResolver != null) {
try {
InputStream stream = FileHelper.getInputStreamFromUriString(url, cordova);
WebResourceResponse response = new WebResourceResponse(mimetype, "UTF-8", stream);
return response;
InputStream stream = uriResolver.getInputStream();
String mimeType = uriResolver.getMimeType();
// If we don't know how to open this file, let the browser continue loading
return new WebResourceResponse(mimeType, "UTF-8", stream);
} catch (IOException e) {
LOG.e("generateWebResourceResponse", e.getMessage(), e);
LOG.e("IceCreamCordovaWebViewClient", "Error occurred while loading a file.", e);
// Results in a 404.
return new WebResourceResponse("text/plain", "UTF-8", null);
}
}
return null;
}
private static boolean needsIceCreamSpecialsInAssetUrlFix(String url) {
if (!url.contains("%20")){
return false;
@@ -96,5 +80,4 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient {
return false;
}
}
}
@@ -0,0 +1,65 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import android.net.Uri;
/*
* Interface for a class that can resolve URIs.
* See CordovaUriResolver for an example.
*/
public interface UriResolver {
/** Returns the URI that this instance will resolve. */
Uri getUri();
/**
* Returns the InputStream for the resource.
* Throws an exception if it cannot be read.
* Never returns null.
*/
InputStream getInputStream() throws IOException;
/**
* Returns the OutputStream for the resource.
* Throws an exception if it cannot be written to.
* Never returns null.
*/
OutputStream getOutputStream() throws IOException;
/**
* Returns the MIME type of the resource.
* Returns null if the MIME type cannot be determined (e.g. content: that doesn't exist).
*/
String getMimeType();
/** Returns whether the resource is writable. */
boolean isWritable();
/**
* Returns a File that points to the resource, or null if the resource
* is not on the local file system.
*/
File getLocalFile();
}
@@ -0,0 +1,277 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.cordova.FileHelper;
import org.apache.http.util.EncodingUtils;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetManager;
import android.net.Uri;
/*
* UriResolver implementations.
*/
public final class UriResolvers {
private UriResolvers() {}
private static final class FileUriResolver implements UriResolver {
private final Uri uri;
private String mimeType;
private File localFile;
FileUriResolver(Uri uri) {
this.uri = uri;
}
public Uri getUri() {
return uri;
}
public InputStream getInputStream() throws IOException {
return new FileInputStream(getLocalFile());
}
public OutputStream getOutputStream() throws FileNotFoundException {
return new FileOutputStream(getLocalFile());
}
public String getMimeType() {
if (mimeType == null) {
mimeType = FileHelper.getMimeTypeForExtension(getLocalFile().getName());
}
return mimeType;
}
public boolean isWritable() {
File f = getLocalFile();
if (f.isDirectory()) {
return false;
}
if (f.exists()) {
return f.canWrite();
}
return f.getParentFile().canWrite();
}
public File getLocalFile() {
if (localFile == null) {
localFile = new File(uri.getPath());
}
return localFile;
}
}
private static final class AssetUriResolver implements UriResolver {
private final Uri uri;
private final AssetManager assetManager;
private final String assetPath;
private String mimeType;
AssetUriResolver(Uri uri, AssetManager assetManager) {
this.uri = uri;
this.assetManager = assetManager;
this.assetPath = uri.getPath().substring(15);
}
public Uri getUri() {
return uri;
}
public InputStream getInputStream() throws IOException {
return assetManager.open(assetPath);
}
public OutputStream getOutputStream() throws FileNotFoundException {
throw new FileNotFoundException("URI not writable.");
}
public String getMimeType() {
if (mimeType == null) {
mimeType = FileHelper.getMimeTypeForExtension(assetPath);
}
return mimeType;
}
public boolean isWritable() {
return false;
}
public File getLocalFile() {
return null;
}
}
private static final class ContentUriResolver implements UriResolver {
private final Uri uri;
private final ContentResolver contentResolver;
private String mimeType;
ContentUriResolver(Uri uri, ContentResolver contentResolver) {
this.uri = uri;
this.contentResolver = contentResolver;
}
public Uri getUri() {
return uri;
}
public InputStream getInputStream() throws IOException {
return contentResolver.openInputStream(uri);
}
public OutputStream getOutputStream() throws FileNotFoundException {
return contentResolver.openOutputStream(uri);
}
public String getMimeType() {
if (mimeType == null) {
mimeType = contentResolver.getType(uri);
}
return mimeType;
}
public boolean isWritable() {
return uri.getScheme().equals(ContentResolver.SCHEME_CONTENT);
}
public File getLocalFile() {
return null;
}
}
static final class ErrorUriResolver implements UriResolver {
final Uri uri;
final String errorMsg;
ErrorUriResolver(Uri uri, String errorMsg) {
this.uri = uri;
this.errorMsg = errorMsg;
}
@Override
public boolean isWritable() {
return false;
}
@Override
public Uri getUri() {
return uri;
}
@Override
public File getLocalFile() {
return null;
}
@Override
public OutputStream getOutputStream() throws IOException {
throw new FileNotFoundException(errorMsg);
}
@Override
public String getMimeType() {
return null;
}
@Override
public InputStream getInputStream() throws IOException {
throw new FileNotFoundException(errorMsg);
}
}
private static final class ReadOnlyResolver implements UriResolver {
private Uri uri;
private InputStream inputStream;
private String mimeType;
public ReadOnlyResolver(Uri uri, InputStream inputStream, String mimeType) {
this.uri = uri;
this.inputStream = inputStream;
this.mimeType = mimeType;
}
@Override
public boolean isWritable() {
return false;
}
@Override
public Uri getUri() {
return uri;
}
@Override
public File getLocalFile() {
return null;
}
@Override
public OutputStream getOutputStream() throws IOException {
throw new FileNotFoundException("URI is not writable");
}
@Override
public String getMimeType() {
return mimeType;
}
@Override
public InputStream getInputStream() throws IOException {
return inputStream;
}
}
public static UriResolver createInline(Uri uri, String response, String mimeType) {
return createInline(uri, EncodingUtils.getBytes(response, "UTF-8"), mimeType);
}
public static UriResolver createInline(Uri uri, byte[] response, String mimeType) {
return new ReadOnlyResolver(uri, new ByteArrayInputStream(response), mimeType);
}
public static UriResolver createReadOnly(Uri uri, InputStream inputStream, String mimeType) {
return new ReadOnlyResolver(uri, inputStream, mimeType);
}
/* Package-private to force clients to go through CordovaWebView.resolveUri(). */
static UriResolver forUri(Uri uri, Context context) {
String scheme = uri.getScheme();
if (ContentResolver.SCHEME_CONTENT.equals(scheme) || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
return new ContentUriResolver(uri, context.getContentResolver());
}
if (ContentResolver.SCHEME_FILE.equals(scheme)) {
if (uri.getPath().startsWith("/android_asset/")) {
return new AssetUriResolver(uri, context.getAssets());
}
return new FileUriResolver(uri);
}
return null;
}
}
@@ -20,14 +20,12 @@ package org.apache.cordova.api;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.UriResolver;
import org.json.JSONArray;
import org.json.JSONException;
import android.annotation.TargetApi;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import android.webkit.WebResourceResponse;
import android.net.Uri;
/**
* Plugins must extend this class and override one of the execute methods.
@@ -165,13 +163,10 @@ public class CordovaPlugin {
}
/**
* By specifying a <url-filter> in config.xml you can map a URL prefix to this method. It applies to all resources loaded in the WebView, not just top-level navigation.
*
* @param url The URL of the resource to be loaded.
* @return Return a WebResourceResponse for the resource, or null to let the WebView handle it normally.
* Hook for overriding the default URI handling mechanism.
* Applies to WebView requests as well as requests made by plugins.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public WebResourceResponse shouldInterceptRequest(String url) {
public UriResolver resolveUri(Uri uri) {
return null;
}
@@ -26,12 +26,14 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.UriResolver;
import org.json.JSONException;
import org.xmlpull.v1.XmlPullParserException;
import android.content.Intent;
import android.content.res.XmlResourceParser;
import android.net.Uri;
import android.util.Log;
import android.webkit.WebResourceResponse;
@@ -379,25 +381,6 @@ public class PluginManager {
return false;
}
/**
* Called when the WebView is loading any resource, top-level or not.
*
* Uses the same url-filter tag as onOverrideUrlLoading.
*
* @param url The URL of the resource to be loaded.
* @return Return a WebResourceResponse with the resource, or null if the WebView should handle it.
*/
public WebResourceResponse shouldInterceptRequest(String url) {
Iterator<Entry<String, String>> it = this.urlMap.entrySet().iterator();
while (it.hasNext()) {
HashMap.Entry<String, String> pairs = it.next();
if (url.startsWith(pairs.getKey())) {
return this.getPlugin(pairs.getValue()).shouldInterceptRequest(url);
}
}
return null;
}
/**
* Called when the app navigates or refreshes.
*/
@@ -419,6 +402,18 @@ public class PluginManager {
LOG.e(TAG, "=====================================================================================");
}
/* Should be package private */ public UriResolver resolveUri(Uri uri) {
for (PluginEntry entry : this.entries.values()) {
if (entry.plugin != null) {
UriResolver ret = entry.plugin.resolveUri(uri);
if (ret != null) {
return ret;
}
}
}
return null;
}
private class PluginManagerService extends CordovaPlugin {
@Override
public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException {