diff --git a/framework/src/org/apache/cordova/CordovaPlugin.java b/framework/src/org/apache/cordova/CordovaPlugin.java index 22c87030..e947fcc5 100644 --- a/framework/src/org/apache/cordova/CordovaPlugin.java +++ b/framework/src/org/apache/cordova/CordovaPlugin.java @@ -22,7 +22,6 @@ import org.apache.cordova.CordovaArgs; import org.apache.cordova.CordovaWebView; import org.apache.cordova.api.CordovaInterface; import org.apache.cordova.api.CallbackContext; -import org.apache.cordova.UriResolver; import org.json.JSONArray; import org.json.JSONException; @@ -165,13 +164,12 @@ public class CordovaPlugin { } /** - * Hook for overriding the default URI handling mechanism. - * Applies to WebView requests as well as requests made by plugins. + * Hook for redirecting requests. Applies to WebView requests as well as requests made by plugins. */ - public UriResolver resolveUri(Uri uri) { + public Uri remapUri(Uri uri) { return null; } - + /** * Called when the WebView does a top-level navigation or refreshes. * diff --git a/framework/src/org/apache/cordova/CordovaResourceApi.java b/framework/src/org/apache/cordova/CordovaResourceApi.java new file mode 100644 index 00000000..b891b51a --- /dev/null +++ b/framework/src/org/apache/cordova/CordovaResourceApi.java @@ -0,0 +1,347 @@ +/* + 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 android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Looper; +import android.util.Base64; +import android.util.Base64InputStream; + +import com.squareup.okhttp.OkHttpClient; + +import org.apache.http.util.EncodingUtils; + +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 java.net.HttpURLConnection; +import java.net.URL; +import java.nio.channels.FileChannel; + +public class CordovaResourceApi { + @SuppressWarnings("unused") + private static final String LOG_TAG = "CordovaResourceApi"; + + public static final int URI_TYPE_FILE = 0; + public static final int URI_TYPE_ASSET = 1; + public static final int URI_TYPE_CONTENT = 2; + public static final int URI_TYPE_RESOURCE = 3; + public static final int URI_TYPE_DATA = 4; + public static final int URI_TYPE_HTTP = 5; + public static final int URI_TYPE_HTTPS = 6; + public static final int URI_TYPE_UNKNOWN = -1; + + private static final String[] LOCAL_FILE_PROJECTION = { "_data" }; + + // Creating this is light-weight. + private static OkHttpClient httpClient = new OkHttpClient(); + + static Thread webCoreThread; + + private final AssetManager assetManager; + private final ContentResolver contentResolver; + private final PluginManager pluginManager; + private boolean threadCheckingEnabled = true; + + + public CordovaResourceApi(Context context, PluginManager pluginManager) { + this.contentResolver = context.getContentResolver(); + this.assetManager = context.getAssets(); + this.pluginManager = pluginManager; + } + + public void setThreadCheckingEnabled(boolean value) { + threadCheckingEnabled = value; + } + + public boolean isThreadCheckingEnabled() { + return threadCheckingEnabled; + } + + public static int getUriType(Uri uri) { + assertNonRelative(uri); + String scheme = uri.getScheme(); + if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { + return URI_TYPE_CONTENT; + } + if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { + return URI_TYPE_RESOURCE; + } + if (ContentResolver.SCHEME_FILE.equals(scheme)) { + if (uri.getPath().startsWith("/android_asset/")) { + return URI_TYPE_ASSET; + } + return URI_TYPE_FILE; + } + if ("data".equals(scheme)) { + return URI_TYPE_DATA; + } + if ("http".equals(scheme)) { + return URI_TYPE_HTTP; + } + if ("https".equals(scheme)) { + return URI_TYPE_HTTPS; + } + return URI_TYPE_UNKNOWN; + } + + public Uri remapUri(Uri uri) { + assertNonRelative(uri); + Uri pluginUri = pluginManager.remapUri(uri); + return pluginUri != null ? pluginUri : uri; + } + + public String remapPath(String path) { + return remapUri(Uri.fromFile(new File(path))).getPath(); + } + + /** + * Returns a File that points to the resource, or null if the resource + * is not on the local filesystem. + */ + public File mapUriToFile(Uri uri) { + assertBackgroundThread(); + switch (getUriType(uri)) { + case URI_TYPE_FILE: + return new File(uri.getPath()); + case URI_TYPE_CONTENT: { + Cursor cursor = contentResolver.query(uri, LOCAL_FILE_PROJECTION, null, null, null); + if (cursor != null) { + try { + int columnIndex = cursor.getColumnIndex(LOCAL_FILE_PROJECTION[0]); + if (columnIndex != -1 && cursor.getCount() > 0) { + cursor.moveToFirst(); + String realPath = cursor.getString(columnIndex); + if (realPath != null) { + return new File(realPath); + } + } + } finally { + cursor.close(); + } + } + } + } + return null; + } + + /** + * Opens a stream to the givne URI, also providing the MIME type & length. + * @return Never returns null. + * @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be + * resolved before being passed into this function. + * @throws Throws an IOException if the URI cannot be opened. + */ + public OpenForReadResult openForRead(Uri uri) throws IOException { + assertBackgroundThread(); + switch (getUriType(uri)) { + case URI_TYPE_FILE: { + FileInputStream inputStream = new FileInputStream(uri.getPath()); + String mimeType = FileHelper.getMimeTypeForExtension(uri.getPath()); + long length = inputStream.getChannel().size(); + return new OpenForReadResult(uri, inputStream, mimeType, length, null); + } + case URI_TYPE_ASSET: { + String assetPath = uri.getPath().substring(15); + AssetFileDescriptor assetFd = null; + InputStream inputStream; + long length = -1; + try { + assetFd = assetManager.openFd(assetPath); + inputStream = assetFd.createInputStream(); + length = assetFd.getLength(); + } catch (FileNotFoundException e) { + // Will occur if the file is compressed. + inputStream = assetManager.open(assetPath); + } + String mimeType = FileHelper.getMimeTypeForExtension(assetPath); + return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd); + } + case URI_TYPE_CONTENT: + case URI_TYPE_RESOURCE: { + String mimeType = contentResolver.getType(uri); + AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, "r"); + InputStream inputStream = assetFd.createInputStream(); + long length = assetFd.getLength(); + return new OpenForReadResult(uri, inputStream, mimeType, length, assetFd); + } + case URI_TYPE_DATA: { + OpenForReadResult ret = readDataUri(uri); + if (ret == null) { + break; + } + return ret; + } + case URI_TYPE_HTTP: + case URI_TYPE_HTTPS: { + HttpURLConnection conn = httpClient.open(new URL(uri.toString())); + conn.setDoInput(true); + String mimeType = conn.getHeaderField("Content-Type"); + int length = conn.getContentLength(); + InputStream inputStream = conn.getInputStream(); + return new OpenForReadResult(uri, inputStream, mimeType, length, null); + } + } + throw new FileNotFoundException("URI not supported by CordovaResourceApi: " + uri); + } + + public OutputStream openOutputStream(Uri uri) throws IOException { + return openOutputStream(uri, false); + } + + /** + * Opens a stream to the given URI. + * @return Never returns null. + * @throws Throws an InvalidArgumentException for relative URIs. Relative URIs should be + * resolved before being passed into this function. + * @throws Throws an IOException if the URI cannot be opened. + */ + public OutputStream openOutputStream(Uri uri, boolean append) throws IOException { + assertBackgroundThread(); + switch (getUriType(uri)) { + case URI_TYPE_FILE: { + File localFile = new File(uri.getPath()); + File parent = localFile.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + return new FileOutputStream(localFile, append); + } + case URI_TYPE_CONTENT: + case URI_TYPE_RESOURCE: { + AssetFileDescriptor assetFd = contentResolver.openAssetFileDescriptor(uri, append ? "wa" : "w"); + return assetFd.createOutputStream(); + } + } + throw new FileNotFoundException("URI not supported by CordovaResourceApi: " + uri); + } + + public HttpURLConnection createHttpConnection(Uri uri) throws IOException { + assertBackgroundThread(); + return httpClient.open(new URL(uri.toString())); + } + + // Copies the input to the output in the most efficient manner possible. + // Closes both streams. + public void copyResource(OpenForReadResult input, OutputStream outputStream) throws IOException { + assertBackgroundThread(); + try { + InputStream inputStream = input.inputStream; + if (inputStream instanceof FileInputStream && outputStream instanceof FileOutputStream) { + FileChannel inChannel = ((FileInputStream)input.inputStream).getChannel(); + FileChannel outChannel = ((FileOutputStream)outputStream).getChannel(); + long offset = 0; + long length = input.length; + if (input.assetFd != null) { + offset = input.assetFd.getStartOffset(); + } + outChannel.transferFrom(inChannel, offset, length); + } else { + final int BUFFER_SIZE = 8192; + byte[] buffer = new byte[BUFFER_SIZE]; + + for (;;) { + int bytesRead = inputStream.read(buffer, 0, BUFFER_SIZE); + + if (bytesRead <= 0) { + break; + } + outputStream.write(buffer, 0, bytesRead); + } + } + } finally { + input.inputStream.close(); + if (outputStream != null) { + outputStream.close(); + } + } + } + + public void copyResource(Uri sourceUri, OutputStream outputStream) throws IOException { + copyResource(openForRead(sourceUri), outputStream); + } + + + private void assertBackgroundThread() { + if (threadCheckingEnabled) { + Thread curThread = Thread.currentThread(); + if (curThread == Looper.getMainLooper().getThread()) { + throw new IllegalStateException("Do not perform IO operations on the UI thread. Use CordovaInterface.getThreadPool() instead."); + } + if (curThread == webCoreThread) { + throw new IllegalStateException("Tried to perform an IO operation on the WebCore thread. Use CordovaInterface.getThreadPool() instead."); + } + } + } + + private OpenForReadResult readDataUri(Uri uri) { + String uriAsString = uri.toString().substring(5); + int commaPos = uriAsString.indexOf(','); + if (commaPos == -1) { + return null; + } + String[] mimeParts = uriAsString.substring(0, commaPos).split(";"); + String contentType = null; + boolean base64 = false; + if (mimeParts.length > 0) { + contentType = mimeParts[0]; + } + for (int i = 1; i < mimeParts.length; ++i) { + if ("base64".equalsIgnoreCase(mimeParts[i])) { + base64 = true; + } + } + String dataPartAsString = uriAsString.substring(commaPos + 1); + byte[] data = base64 ? Base64.decode(dataPartAsString, Base64.DEFAULT) : EncodingUtils.getBytes(dataPartAsString, "UTF-8"); + InputStream inputStream = new ByteArrayInputStream(data); + return new OpenForReadResult(uri, inputStream, contentType, data.length, null); + } + + private static void assertNonRelative(Uri uri) { + if (!uri.isAbsolute()) { + throw new IllegalArgumentException("Relative URIs are not supported."); + } + } + + public static final class OpenForReadResult { + public final Uri uri; + public final InputStream inputStream; + public final String mimeType; + public final long length; + public final AssetFileDescriptor assetFd; + + OpenForReadResult(Uri uri, InputStream inputStream, String mimeType, long length, AssetFileDescriptor assetFd) { + this.uri = uri; + this.inputStream = inputStream; + this.mimeType = mimeType; + this.length = length; + this.assetFd = assetFd; + } + } +} diff --git a/framework/src/org/apache/cordova/CordovaWebView.java b/framework/src/org/apache/cordova/CordovaWebView.java index 648b1f88..b97b03e0 100755 --- a/framework/src/org/apache/cordova/CordovaWebView.java +++ b/framework/src/org/apache/cordova/CordovaWebView.java @@ -97,6 +97,8 @@ public class CordovaWebView extends WebView { private ActivityResult mResult = null; + private CordovaResourceApi resourceApi; + class ActivityResult { int request; @@ -307,6 +309,7 @@ public class CordovaWebView extends WebView { pluginManager = new PluginManager(this, this.cordova); jsMessageQueue = new NativeToJsMessageQueue(this, cordova); exposedJsApi = new ExposedJsApi(pluginManager, jsMessageQueue); + resourceApi = new CordovaResourceApi(this.getContext(), pluginManager); exposeJsInterface(); } @@ -957,37 +960,7 @@ public class CordovaWebView extends WebView { 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."); - } - UriResolver ret = null; - // 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); - ret = UriResolvers.createError("Whitelist rejection for: " + uri); - } else { - // Give plugins a chance to handle the request. - ret = ((org.apache.cordova.PluginManager)pluginManager).resolveUri(uri); - } - if (ret == null && !fromWebView) { - ret = UriResolvers.forUri(uri, cordova.getActivity()); - if (ret == null) { - ret = UriResolvers.createError("Unresolvable URI: " + uri); - } - } - return ret == null ? null : UriResolvers.makeThreadChecking(ret); + public CordovaResourceApi getResourceApi() { + return resourceApi; } } diff --git a/framework/src/org/apache/cordova/CordovaWebViewClient.java b/framework/src/org/apache/cordova/CordovaWebViewClient.java index c49611e6..d782aa04 100755 --- a/framework/src/org/apache/cordova/CordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/CordovaWebViewClient.java @@ -48,7 +48,7 @@ import android.webkit.WebViewClient; */ public class CordovaWebViewClient extends WebViewClient { - private static final String TAG = "Cordova"; + private static final String TAG = "CordovaWebViewClient"; private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/"; CordovaInterface cordova; CordovaWebView appView; diff --git a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java index c23d5807..317acc2f 100644 --- a/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java +++ b/framework/src/org/apache/cordova/IceCreamCordovaWebViewClient.java @@ -19,20 +19,22 @@ package org.apache.cordova; import java.io.IOException; -import java.io.InputStream; +import org.apache.cordova.CordovaResourceApi.OpenForReadResult; 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.util.Log; import android.webkit.WebResourceResponse; import android.webkit.WebView; @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class IceCreamCordovaWebViewClient extends CordovaWebViewClient { + private static final String TAG = "IceCreamCordovaWebViewClient"; public IceCreamCordovaWebViewClient(CordovaInterface cordova) { super(cordova); @@ -44,38 +46,44 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient { @Override public WebResourceResponse shouldInterceptRequest(WebView view, String url) { - // Disable checks during shouldInterceptRequest since there is no way to avoid IO here :(. - UriResolvers.webCoreThread = null; + // Tell the Thread-Checking resolve what thread the WebCore thread is. + CordovaResourceApi.webCoreThread = Thread.currentThread(); + Log.e("WHAAAA", "FOOD " + CordovaResourceApi.webCoreThread); try { - UriResolver uriResolver = appView.resolveUri(Uri.parse(url), true); - - if (uriResolver == null && url.startsWith("file:///android_asset/")) { - if (url.contains("?") || url.contains("#") || needsIceCreamSpecialsInAssetUrlFix(url)) { - uriResolver = appView.resolveUri(Uri.parse(url), false); - } + // Check the against the white-list. + if ((url.startsWith("http:") || url.startsWith("https:")) && !Config.isUrlWhiteListed(url)) { + LOG.w(TAG, "URL blocked by whitelist: " + url); + // Results in a 404. + return new WebResourceResponse("text/plain", "UTF-8", null); } + + CordovaResourceApi resourceApi = appView.getResourceApi(); + Uri origUri = Uri.parse(url); + // Allow plugins to intercept WebView requests. + Uri remappedUri = resourceApi.remapUri(origUri); - if (uriResolver != null) { - try { - 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("IceCreamCordovaWebViewClient", "Error occurred while loading a file.", e); - // Results in a 404. - return new WebResourceResponse("text/plain", "UTF-8", null); - } + if (!origUri.equals(remappedUri) || needsSpecialsInAssetUrlFix(origUri)) { + OpenForReadResult result = resourceApi.openForRead(remappedUri); + return new WebResourceResponse(result.mimeType, "UTF-8", result.inputStream); } + // If we don't need to special-case the request, let the browser load it. return null; - } finally { - // Tell the Thread-Checking resolve what thread the WebCore thread is. - UriResolvers.webCoreThread = Thread.currentThread(); + } catch (IOException e) { + LOG.e("IceCreamCordovaWebViewClient", "Error occurred while loading a file.", e); + // Results in a 404. + return new WebResourceResponse("text/plain", "UTF-8", null); } } + + private static boolean needsSpecialsInAssetUrlFix(Uri uri) { + if (CordovaResourceApi.getUriType(uri) != CordovaResourceApi.URI_TYPE_ASSET) { + return false; + } + if (uri.getQuery() != null || uri.getFragment() != null) { + return true; + } - private static boolean needsIceCreamSpecialsInAssetUrlFix(String url) { - if (!url.contains("%20")){ + if (!uri.toString().contains("%")) { return false; } @@ -83,8 +91,7 @@ public class IceCreamCordovaWebViewClient extends CordovaWebViewClient { case android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH: case android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1: return true; - default: - return false; } + return false; } } diff --git a/framework/src/org/apache/cordova/PluginManager.java b/framework/src/org/apache/cordova/PluginManager.java index 1f32ba6b..2db9d56e 100755 --- a/framework/src/org/apache/cordova/PluginManager.java +++ b/framework/src/org/apache/cordova/PluginManager.java @@ -26,7 +26,6 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.cordova.CordovaArgs; import org.apache.cordova.CordovaWebView; -import org.apache.cordova.UriResolver; import org.apache.cordova.api.CallbackContext; import org.apache.cordova.api.CordovaInterface; import org.apache.cordova.api.CordovaPlugin; @@ -40,7 +39,6 @@ import android.content.res.XmlResourceParser; import android.net.Uri; import android.util.Log; -import android.webkit.WebResourceResponse; /** * PluginManager is exposed to JavaScript in the Cordova WebView. @@ -407,10 +405,10 @@ public class PluginManager { LOG.e(TAG, "====================================================================================="); } - UriResolver resolveUri(Uri uri) { + Uri remapUri(Uri uri) { for (PluginEntry entry : this.entries.values()) { if (entry.plugin != null) { - UriResolver ret = entry.plugin.resolveUri(uri); + Uri ret = entry.plugin.remapUri(uri); if (ret != null) { return ret; } diff --git a/framework/src/org/apache/cordova/UriResolver.java b/framework/src/org/apache/cordova/UriResolver.java deleted file mode 100644 index b3bfa4d4..00000000 --- a/framework/src/org/apache/cordova/UriResolver.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - 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; - -/* - * Interface for a class that can resolve URIs. - * See CordovaUriResolver for an example. - */ -public abstract class UriResolver { - - /** - * Returns the InputStream for the resource. - * Throws an exception if it cannot be read. - * Never returns null. - */ - public abstract InputStream getInputStream() 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). - */ - public abstract String getMimeType(); - - /** Returns whether the resource is writable. */ - public abstract boolean isWritable(); - - /** - * Returns a File that points to the resource, or null if the resource - * is not on the local file system. - */ - public abstract File getLocalFile(); - - /** - * Returns the OutputStream for the resource. - * Throws an exception if it cannot be written to. - * Never returns null. - */ - public OutputStream getOutputStream() throws IOException { - throw new IOException("Writing is not suppported"); - } - - /** - * Returns the length of the input stream, or -1 if it is not computable. - */ - public long computeLength() throws IOException { - return -1; - } -} diff --git a/framework/src/org/apache/cordova/UriResolvers.java b/framework/src/org/apache/cordova/UriResolvers.java deleted file mode 100644 index 294fc6bf..00000000 --- a/framework/src/org/apache/cordova/UriResolvers.java +++ /dev/null @@ -1,341 +0,0 @@ -/* - 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; -import android.os.Looper; - -/* - * UriResolver implementations. - */ -public final class UriResolvers { - static Thread webCoreThread; - - private UriResolvers() {} - - private static long computeSizeFromResolver(UriResolver resolver) throws IOException { - InputStream inputStream = resolver.getInputStream(); - if (inputStream instanceof FileInputStream) { - return ((FileInputStream)inputStream).getChannel().size(); - } - if (inputStream instanceof ByteArrayInputStream) { - return ((ByteArrayInputStream)inputStream).available(); - } - return -1; - } - - private static final class FileUriResolver extends UriResolver { - private final File localFile; - private String mimeType; - private FileInputStream cachedInputStream; - - FileUriResolver(Uri uri) { - localFile = new File(uri.getPath()); - } - - public InputStream getInputStream() throws IOException { - if (cachedInputStream == null) { - cachedInputStream = new FileInputStream(localFile); - } - return cachedInputStream; - } - - public OutputStream getOutputStream() throws FileNotFoundException { - File parent = localFile.getParentFile(); - if (parent != null) { - localFile.getParentFile().mkdirs(); - } - return new FileOutputStream(localFile); - } - - public String getMimeType() { - if (mimeType == null) { - mimeType = FileHelper.getMimeTypeForExtension(localFile.getName()); - } - return mimeType; - } - - public boolean isWritable() { - if (localFile.isDirectory()) { - return false; - } - if (localFile.exists()) { - return localFile.canWrite(); - } - return localFile.getParentFile().canWrite(); - } - - public File getLocalFile() { - return localFile; - } - - public long computeLength() throws IOException { - return localFile.length(); - } - } - - private static final class AssetUriResolver extends UriResolver { - private final AssetManager assetManager; - private final String assetPath; - private String mimeType; - private InputStream cachedInputStream; - - AssetUriResolver(Uri uri, AssetManager assetManager) { - this.assetManager = assetManager; - this.assetPath = uri.getPath().substring(15); - } - - public InputStream getInputStream() throws IOException { - if (cachedInputStream == null) { - cachedInputStream = assetManager.open(assetPath); - } - return cachedInputStream; - } - - 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; - } - - public long computeLength() throws IOException { - return computeSizeFromResolver(this); - } - } - - private static final class ContentUriResolver extends UriResolver { - private final Uri uri; - private final ContentResolver contentResolver; - private String mimeType; - private InputStream cachedInputStream; - - ContentUriResolver(Uri uri, ContentResolver contentResolver) { - this.uri = uri; - this.contentResolver = contentResolver; - } - - public InputStream getInputStream() throws IOException { - if (cachedInputStream == null) { - cachedInputStream = contentResolver.openInputStream(uri); - } - return cachedInputStream; - } - - 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; - } - - public long computeLength() throws IOException { - return computeSizeFromResolver(this); - } - } - - private static final class ErrorUriResolver extends UriResolver { - final String errorMsg; - - ErrorUriResolver(String errorMsg) { - this.errorMsg = errorMsg; - } - - public boolean isWritable() { - return false; - } - - public File getLocalFile() { - return null; - } - - public OutputStream getOutputStream() throws IOException { - throw new FileNotFoundException(errorMsg); - } - - public String getMimeType() { - return null; - } - - public InputStream getInputStream() throws IOException { - throw new FileNotFoundException(errorMsg); - } - } - - private static final class ReadOnlyResolver extends UriResolver { - private InputStream inputStream; - private String mimeType; - - public ReadOnlyResolver(Uri uri, InputStream inputStream, String mimeType) { - this.inputStream = inputStream; - this.mimeType = mimeType; - } - - public boolean isWritable() { - return false; - } - - public File getLocalFile() { - return null; - } - - public OutputStream getOutputStream() throws IOException { - throw new FileNotFoundException("URI is not writable"); - } - - public String getMimeType() { - return mimeType; - } - - public InputStream getInputStream() throws IOException { - return inputStream; - } - - public long computeLength() throws IOException { - return computeSizeFromResolver(this); - } - } - - private static final class ThreadCheckingResolver extends UriResolver { - final UriResolver delegate; - - ThreadCheckingResolver(UriResolver delegate) { - this.delegate = delegate; - } - - private static void checkThread() { - Thread curThread = Thread.currentThread(); - if (curThread == Looper.getMainLooper().getThread()) { - throw new IllegalStateException("Do not perform IO operations on the UI thread. Use CordovaInterface.getThreadPool() instead."); - } - if (curThread == webCoreThread) { - throw new IllegalStateException("Tried to perform an IO operation on the WebCore thread. Use CordovaInterface.getThreadPool() instead."); - } - } - - public boolean isWritable() { - checkThread(); - return delegate.isWritable(); - } - - - public File getLocalFile() { - checkThread(); - return delegate.getLocalFile(); - } - - public OutputStream getOutputStream() throws IOException { - checkThread(); - return delegate.getOutputStream(); - } - - public String getMimeType() { - checkThread(); - return delegate.getMimeType(); - } - - public InputStream getInputStream() throws IOException { - checkThread(); - return delegate.getInputStream(); - } - - public long computeLength() throws IOException { - checkThread(); - return delegate.computeLength(); - } - } - - 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); - } - - public static UriResolver createError(String errorMsg) { - return new ErrorUriResolver(errorMsg); - } - - /* 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; - } - - /* Used only by CordovaWebView.resolveUri(). */ - static UriResolver makeThreadChecking(UriResolver resolver) { - if (resolver instanceof ThreadCheckingResolver) { - return resolver; - } - return new ThreadCheckingResolver(resolver); - } -} diff --git a/test/AndroidManifest.xml b/test/AndroidManifest.xml index f6c840eb..04ef3c6e 100755 --- a/test/AndroidManifest.xml +++ b/test/AndroidManifest.xml @@ -45,7 +45,7 @@ - + { - - public UriResolversTest() - { - super(CordovaWebViewTestActivity.class); - } - - CordovaWebView cordovaWebView; - private CordovaWebViewTestActivity activity; - String execPayload; - Integer execStatus; - - protected void setUp() throws Exception { - super.setUp(); - activity = this.getActivity(); - cordovaWebView = activity.cordovaWebView; - cordovaWebView.pluginManager.addService(new PluginEntry("UriResolverTestPlugin1", new CordovaPlugin() { - @Override - public UriResolver resolveUri(Uri uri) { - if ("plugin-uri".equals(uri.getScheme())) { - return cordovaWebView.resolveUri(uri.buildUpon().scheme("file").build()); - } - return null; - } - public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { - synchronized (UriResolversTest.this) { - execPayload = args.getString(0); - execStatus = args.getInt(1); - UriResolversTest.this.notify(); - } - return true; - } - })); - cordovaWebView.pluginManager.addService(new PluginEntry("UriResolverTestPlugin2", new CordovaPlugin() { - @Override - public UriResolver resolveUri(Uri uri) { - if (uri.getQueryParameter("pluginRewrite") != null) { - return UriResolvers.createInline(uri, "pass", "my/mime"); - } - return null; - } - })); - } - - private Uri createTestImageContentUri() { - Bitmap imageBitmap = BitmapFactory.decodeResource(activity.getResources(), R.drawable.icon); - String stored = MediaStore.Images.Media.insertImage(activity.getContentResolver(), - imageBitmap, "app-icon", "desc"); - return Uri.parse(stored); - } - - private void performResolverTest(Uri uri, String expectedMimeType, File expectedLocalFile, - boolean expectedIsWritable, - boolean expectRead, boolean expectWrite) throws IOException { - UriResolver resolver = cordovaWebView.resolveUri(uri); - assertEquals(expectedLocalFile, resolver.getLocalFile()); - assertEquals(expectedMimeType, resolver.getMimeType()); - if (expectedIsWritable) { - assertTrue(resolver.isWritable()); - } else { - assertFalse(resolver.isWritable()); - } - try { - resolver.getInputStream().read(); - if (!expectRead) { - fail("Expected getInputStream to throw."); - } - } catch (IOException e) { - if (expectRead) { - throw e; - } - } - try { - resolver.getOutputStream().write(123); - if (!expectWrite) { - fail("Expected getOutputStream to throw."); - } - } catch (IOException e) { - if (expectWrite) { - throw e; - } - } - } - - public void testValidContentUri() throws IOException - { - Uri contentUri = createTestImageContentUri(); - performResolverTest(contentUri, "image/jpeg", null, true, true, true); - } - - public void testInvalidContentUri() throws IOException - { - Uri contentUri = Uri.parse("content://media/external/images/media/999999999"); - performResolverTest(contentUri, null, null, true, false, false); - } - - public void testValidAssetUri() throws IOException - { - Uri assetUri = Uri.parse("file:///android_asset/www/index.html?foo#bar"); // Also check for stripping off ? and # correctly. - performResolverTest(assetUri, "text/html", null, false, true, false); - } - - public void testInvalidAssetUri() throws IOException - { - Uri assetUri = Uri.parse("file:///android_asset/www/missing.html"); - performResolverTest(assetUri, "text/html", null, false, false, false); - } - - public void testFileUriToExistingFile() throws IOException - { - File f = File.createTempFile("te s t", ".txt"); // Also check for dealing with spaces. - try { - Uri fileUri = Uri.parse(f.toURI().toString() + "?foo#bar"); // Also check for stripping off ? and # correctly. - performResolverTest(fileUri, "text/plain", f, true, true, true); - } finally { - f.delete(); - } - } - - public void testFileUriToMissingFile() throws IOException - { - File f = new File(Environment.getExternalStorageDirectory() + "/somefilethatdoesntexist"); - Uri fileUri = Uri.parse(f.toURI().toString()); - try { - performResolverTest(fileUri, null, f, true, false, true); - } finally { - f.delete(); - } - } - - public void testFileUriToMissingFileWithMissingParent() throws IOException - { - File f = new File(Environment.getExternalStorageDirectory() + "/somedirthatismissing/somefilethatdoesntexist"); - Uri fileUri = Uri.parse(f.toURI().toString()); - performResolverTest(fileUri, null, f, false, false, false); - } - - public void testUnrecognizedUri() throws IOException - { - Uri uri = Uri.parse("somescheme://foo"); - performResolverTest(uri, null, null, false, false, false); - } - - public void testRelativeUri() - { - try { - cordovaWebView.resolveUri(Uri.parse("/foo")); - fail("Should have thrown for relative URI 1."); - } catch (Throwable t) { - } - try { - cordovaWebView.resolveUri(Uri.parse("//foo/bar")); - fail("Should have thrown for relative URI 2."); - } catch (Throwable t) { - } - try { - cordovaWebView.resolveUri(Uri.parse("foo.png")); - fail("Should have thrown for relative URI 3."); - } catch (Throwable t) { - } - } - - public void testPluginOverrides1() throws IOException - { - Uri uri = Uri.parse("plugin-uri://foohost/android_asset/www/index.html"); - performResolverTest(uri, "text/html", null, false, true, false); - } - - public void testPluginOverrides2() throws IOException - { - Uri uri = Uri.parse("plugin-uri://foohost/android_asset/www/index.html?pluginRewrite=yes"); - performResolverTest(uri, "my/mime", null, false, true, false); - } - - public void testWhitelistRejection() throws IOException - { - Uri uri = Uri.parse("http://foohost.com/"); - performResolverTest(uri, null, null, false, false, false); - } - - public void testWebViewRequestIntercept() throws IOException - { - cordovaWebView.sendJavascript( - "var x = new XMLHttpRequest;\n" + - "x.open('GET', 'file://foo?pluginRewrite=1', false);\n" + - "x.send();\n" + - "cordova.require('cordova/exec')(null,null,'UriResolverTestPlugin1', 'foo', [x.responseText, x.status])"); - execPayload = null; - execStatus = null; - try { - synchronized (this) { - this.wait(2000); - } - } catch (InterruptedException e) { - } - assertEquals("pass", execPayload); - assertEquals(execStatus.intValue(), 200); - } - - public void testWebViewWhiteListRejection() throws IOException - { - cordovaWebView.sendJavascript( - "var x = new XMLHttpRequest;\n" + - "x.open('GET', 'http://foo/bar', false);\n" + - "x.send();\n" + - "cordova.require('cordova/exec')(null,null,'UriResolverTestPlugin1', 'foo', [x.responseText, x.status])"); - execPayload = null; - execStatus = null; - try { - synchronized (this) { - this.wait(2000); - } - } catch (InterruptedException e) { - } - assertEquals("", execPayload); - assertEquals(execStatus.intValue(), 404); - } -}