Implement Android WebView BlockNetworkImages

This uses the WebCore ImagesEnabled setting, which is added to
ContentSettings. The setting also includes overriding
WebPermissionClient::allowImage, which here allows images with local
soucres to load as well.

Use a whitelist of local file schemes instead of blacklisting only
http(s).

BUG=


Review URL: https://chromiumcodereview.appspot.com/10920033

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@158809 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/AwSettingsTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/AwSettingsTest.java
index f57cffd..70b14bd 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/AwSettingsTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/AwSettingsTest.java
@@ -7,6 +7,7 @@
 import android.content.Context;
 import android.os.Build;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Pair;
 
 import org.chromium.base.test.util.Feature;
 import org.chromium.base.test.util.TestFileUtil;
@@ -14,10 +15,15 @@
 import org.chromium.content.browser.ContentSettings;
 import org.chromium.content.browser.ContentViewCore;
 import org.chromium.content.browser.test.util.CallbackHelper;
+import org.chromium.content.browser.test.util.Criteria;
+import org.chromium.content.browser.test.util.CriteriaHelper;
 import org.chromium.content.browser.test.util.HistoryUtils;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.ArrayList;
+import java.util.List;
+
 
 /**
  * A test suite for ContentSettings class. The key objective is to verify that each
@@ -25,6 +31,8 @@
  * application.
  */
 public class AwSettingsTest extends AndroidWebViewTestBase {
+    private static final int CHECK_INTERVAL = 100;
+
     private static final boolean ENABLED = true;
     private static final boolean DISABLED = false;
 
@@ -1334,4 +1342,84 @@
     private String createContentUrl(final String target) {
         return TestContentProvider.createContentUrl(target);
     }
+
+    private final String IMAGE_DATA = "iVBORw0KGgoAAA" +
+        "ANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAAAXNSR0IArs4c6QAAAA1JREFUCB0BAgD9/wAAAAIAAc3j" +
+        "0SsAAAAASUVORK5CYII=";
+
+    private final String DATA_URL_IMAGE_HTML = "<html>" +
+        "<head><script>function updateTitle(){" +
+        "document.title=document.getElementById('img').naturalHeight;}</script></head>" +
+        "<body onload='updateTitle()'>" +
+        "<img id='img' onload='updateTitle()' src='data:image/png;base64," + IMAGE_DATA +
+        "'></body></html>";
+
+    @SmallTest
+    @Feature({"Android-WebView", "Preferences"})
+    public void testBlockNetworkImagesDoesNotBlockDataUrlImage() throws Throwable {
+        final TestAwContentsClient contentClient = new TestAwContentsClient();
+        final ContentViewCore contentView =
+                createAwTestContainerViewOnMainSync(false, contentClient).getContentViewCore();
+        final ContentSettings settings = getContentSettingsOnUiThread(contentView);
+
+        settings.setJavaScriptEnabled(true);
+
+        settings.setImagesEnabled(false);
+        loadDataSync(contentView,
+                     contentClient.getOnPageFinishedHelper(),
+                     DATA_URL_IMAGE_HTML,
+                     "text/html",
+                     false);
+        assertEquals("1", getTitleOnUiThread(contentView));
+    }
+
+    @SmallTest
+    @Feature({"Android-WebView", "Preferences"})
+    public void testBlockNetworkImagesBlocksNetworkImageAndReloadInPlace() throws Throwable {
+        final TestAwContentsClient contentClient = new TestAwContentsClient();
+        final ContentViewCore contentView =
+                createAwTestContainerViewOnMainSync(false, contentClient).getContentViewCore();
+        final ContentSettings settings = getContentSettingsOnUiThread(contentView);
+        settings.setJavaScriptEnabled(true);
+
+        TestWebServer webServer = null;
+        try {
+            webServer = new TestWebServer(false);
+            List<Pair<String, String>> imageHeaders = new ArrayList<Pair<String, String>>();
+            imageHeaders.add(Pair.create("Content-Type", "image/png"));
+            final String imagePath = "/image.png";
+            webServer.setResponseBase64(imagePath, IMAGE_DATA, imageHeaders);
+
+            final String pagePath = "/html_image.html";
+            final String httpUrlImageHtml = "<html>" +
+                "<head><script>" +
+                "function updateTitle(){" +
+                "document.title=document.getElementById('img').naturalHeight;}" +
+                "</script></head>" +
+                "<body onload='updateTitle()'>" +
+                "<img id='img' onload='updateTitle()' src='" + imagePath +
+                "'></body></html>";
+            final String httpImageUrl = webServer.setResponse(pagePath, httpUrlImageHtml, null);
+
+            settings.setImagesEnabled(false);
+            loadUrlSync(contentView, contentClient.getOnPageFinishedHelper(), httpImageUrl);
+            assertEquals("0", getTitleOnUiThread(contentView));
+
+            settings.setImagesEnabled(true);
+            assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
+                @Override
+                public boolean isSatisfied() {
+                    try {
+                        return "0".equals(getTitleOnUiThread(contentView));
+                    } catch (Throwable t) {
+                        t.printStackTrace();
+                        fail("Failed to getTitleOnUIThread: " + t.toString());
+                        return false;
+                    }
+                }
+            }, WAIT_TIMEOUT_SECONDS * 1000, CHECK_INTERVAL));
+        } finally {
+            if (webServer != null) webServer.shutdown();
+        }
+    }
 }
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/TestWebServer.java b/android_webview/javatests/src/org/chromium/android_webview/test/TestWebServer.java
index 8508a51..0e56f56 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/TestWebServer.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/TestWebServer.java
@@ -15,7 +15,7 @@
 import org.apache.http.HttpVersion;
 import org.apache.http.RequestLine;
 import org.apache.http.StatusLine;
-import org.apache.http.entity.StringEntity;
+import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.impl.DefaultHttpServerConnection;
 import org.apache.http.impl.cookie.DateUtils;
 import org.apache.http.message.BasicHttpResponse;
@@ -26,7 +26,6 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
 import java.net.ServerSocket;
 import java.net.Socket;
@@ -72,11 +71,11 @@
     private boolean mSsl;
 
     private static class Response {
-        final String mResponseStr;
+        final byte[] mResponseData;
         final List<Pair<String, String>> mResponseHeaders;
 
-        Response(String responseStr, List<Pair<String, String>> responseHeaders) {
-            mResponseStr = responseStr;
+        Response(byte[] resposneData, List<Pair<String, String>> responseHeaders) {
+            mResponseData = resposneData;
             mResponseHeaders = responseHeaders == null ?
                     new ArrayList<Pair<String, String>>() : responseHeaders;
         }
@@ -146,16 +145,37 @@
      * in (with the option to specify additional headers).
      *
      * @param requestPath The path to respond to.
-     * @param resposneString The response body that will be returned.
+     * @param responseString The response body that will be returned.
      * @param responseHeaders Any additional headers that should be returned along with the
      *                        response (null is acceptable).
      * @return The full URL including the path that should be requested to get the expected
      *         response.
      */
     public String setResponse(
-            String requestPath, String resposneString,
+            String requestPath, String responseString,
             List<Pair<String, String>> responseHeaders) {
-        mResponseMap.put(requestPath, new Response(resposneString, responseHeaders));
+        mResponseMap.put(requestPath, new Response(responseString.getBytes(), responseHeaders));
+        return mServerUri + requestPath;
+    }
+
+    /**
+     * Sets a base64 encoded response to be returned when a particular request path is passed
+     * in (with the option to specify additional headers).
+     *
+     * @param requestPath The path to respond to.
+     * @param base64EncodedResponse The response body that is base64 encoded. The actual server
+     *                              response will the decoded binary form.
+     * @param responseHeaders Any additional headers that should be returned along with the
+     *                        response (null is acceptable).
+     * @return The full URL including the path that should be requested to get the expected
+     *         response.
+     */
+    public String setResponseBase64(
+            String requestPath, String base64EncodedResponse,
+            List<Pair<String, String>> responseHeaders) {
+        mResponseMap.put(requestPath,
+                new Response(Base64.decode(base64EncodedResponse, Base64.DEFAULT),
+                responseHeaders));
         return mServerUri + requestPath;
     }
 
@@ -234,7 +254,7 @@
             httpResponse = createResponse(HttpStatus.SC_NOT_FOUND);
         } else {
             httpResponse = createResponse(HttpStatus.SC_OK);
-            httpResponse.setEntity(createEntity(response.mResponseStr));
+            httpResponse.setEntity(createEntity(response.mResponseData));
             for (Pair<String, String> header : response.mResponseHeaders) {
                 httpResponse.addHeader(header.first, header.second);
             }
@@ -272,7 +292,7 @@
             buf.append("</title></head><body>");
             buf.append(reason);
             buf.append("</body></html>");
-            response.setEntity(createEntity(buf.toString()));
+            response.setEntity(createEntity(buf.toString().getBytes()));
         }
         return response;
     }
@@ -280,15 +300,10 @@
     /**
      * Create a string entity for the given content.
      */
-    private StringEntity createEntity(String content) {
-        try {
-            StringEntity entity = new StringEntity(content);
-            entity.setContentType("text/html");
-            return entity;
-        } catch (UnsupportedEncodingException e) {
-            Log.w(TAG, e);
-        }
-        return null;
+    private ByteArrayEntity createEntity(byte[] data) {
+        ByteArrayEntity entity = new ByteArrayEntity(data);
+        entity.setContentType("text/html");
+        return entity;
     }
 
     private static class ServerThread extends Thread {
diff --git a/android_webview/renderer/aw_render_view_ext.cc b/android_webview/renderer/aw_render_view_ext.cc
index ae0035d..7f0825c 100644
--- a/android_webview/renderer/aw_render_view_ext.cc
+++ b/android_webview/renderer/aw_render_view_ext.cc
@@ -5,17 +5,20 @@
 #include "android_webview/renderer/aw_render_view_ext.h"
 
 #include "android_webview/common/render_view_messages.h"
+#include "content/public/common/url_constants.h"
 #include "content/public/renderer/render_view.h"
+#include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebURL.h"
+#include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebVector.h"
 #include "third_party/WebKit/Source/WebKit/chromium/public/WebDocument.h"
 #include "third_party/WebKit/Source/WebKit/chromium/public/WebElement.h"
 #include "third_party/WebKit/Source/WebKit/chromium/public/WebFrame.h"
 #include "third_party/WebKit/Source/WebKit/chromium/public/WebView.h"
-#include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebVector.h"
 
 namespace android_webview {
 
 AwRenderViewExt::AwRenderViewExt(content::RenderView* render_view)
     : content::RenderViewObserver(render_view) {
+  render_view->GetWebView()->setPermissionClient(this);
 }
 
 AwRenderViewExt::~AwRenderViewExt() {}
@@ -48,4 +51,19 @@
                                                    hasImages));
 }
 
+bool AwRenderViewExt::allowImage(WebKit::WebFrame* frame,
+                                 bool enabled_per_settings,
+                                 const WebKit::WebURL& image_url) {
+  // Implementing setBlockNetworkImages, so allow local scheme images to be
+  // loaded.
+  if (enabled_per_settings)
+    return true;
+
+  // For compatibility, only blacklist network schemes instead of whitelisting.
+  const GURL url(image_url);
+  return !(url.SchemeIs(chrome::kHttpScheme) ||
+           url.SchemeIs(chrome::kHttpsScheme) ||
+           url.SchemeIs(chrome::kFtpScheme));
+}
+
 }  // namespace android_webview
diff --git a/android_webview/renderer/aw_render_view_ext.h b/android_webview/renderer/aw_render_view_ext.h
index fa58872..fadb457 100644
--- a/android_webview/renderer/aw_render_view_ext.h
+++ b/android_webview/renderer/aw_render_view_ext.h
@@ -8,13 +8,21 @@
 #include "base/basictypes.h"
 #include "base/compiler_specific.h"
 #include "content/public/renderer/render_view_observer.h"
+#include "third_party/WebKit/Source/WebKit/chromium/public/WebPermissionClient.h"
+
+namespace WebKit {
+
+class WebURL;
+
+}  // namespace WebKit
 
 namespace android_webview {
 
 // Render process side of AwRenderViewHostExt, this provides cross-process
 // implementation of miscellaneous WebView functions that we need to poke
 // WebKit directly to implement (and that aren't needed in the chrome app).
-class AwRenderViewExt : public content::RenderViewObserver {
+class AwRenderViewExt : public content::RenderViewObserver,
+                        public WebKit::WebPermissionClient {
  public:
   static void RenderViewCreated(content::RenderView* render_view);
 
@@ -27,6 +35,11 @@
 
   void OnDocumentHasImagesRequest(int id);
 
+  // WebKit::WebPermissionClient implementation.
+  virtual bool allowImage(WebKit::WebFrame* frame,
+                          bool enabledPerSettings,
+                          const WebKit::WebURL& imageURL);
+
   DISALLOW_COPY_AND_ASSIGN(AwRenderViewExt);
 };
 
diff --git a/content/browser/android/content_settings.cc b/content/browser/android/content_settings.cc
index a436c38..e8632d6 100644
--- a/content/browser/android/content_settings.cc
+++ b/content/browser/android/content_settings.cc
@@ -65,6 +65,8 @@
         GetFieldID(env, clazz, "mDefaultFixedFontSize", "I");
     load_images_automatically =
         GetFieldID(env, clazz, "mLoadsImagesAutomatically", "Z");
+    images_enabled =
+        GetFieldID(env, clazz, "mImagesEnabled", "Z");
     java_script_enabled =
         GetFieldID(env, clazz, "mJavaScriptEnabled", "Z");
     allow_universal_access_from_file_urls =
@@ -95,6 +97,7 @@
   jfieldID default_font_size;
   jfieldID default_fixed_font_size;
   jfieldID load_images_automatically;
+  jfieldID images_enabled;
   jfieldID java_script_enabled;
   jfieldID allow_universal_access_from_file_urls;
   jfieldID allow_file_access_from_file_urls;
@@ -197,6 +200,11 @@
   CheckException(env);
 
   env->SetBooleanField(
+      obj,
+      field_ids_->images_enabled, prefs.images_enabled);
+  CheckException(env);
+
+  env->SetBooleanField(
       obj, field_ids_->java_script_enabled, prefs.javascript_enabled);
   CheckException(env);
 
@@ -294,6 +302,9 @@
   prefs.loads_images_automatically =
       env->GetBooleanField(obj, field_ids_->load_images_automatically);
 
+  prefs.images_enabled =
+      env->GetBooleanField(obj, field_ids_->images_enabled);
+
   prefs.javascript_enabled =
       env->GetBooleanField(obj, field_ids_->java_script_enabled);
 
diff --git a/content/public/android/java/src/org/chromium/content/browser/ContentSettings.java b/content/public/android/java/src/org/chromium/content/browser/ContentSettings.java
index 7ae55c7..f3b858b 100644
--- a/content/public/android/java/src/org/chromium/content/browser/ContentSettings.java
+++ b/content/public/android/java/src/org/chromium/content/browser/ContentSettings.java
@@ -80,6 +80,7 @@
     private int mDefaultFontSize = 16;
     private int mDefaultFixedFontSize = 13;
     private boolean mLoadsImagesAutomatically = true;
+    private boolean mImagesEnabled = true;
     private boolean mJavaScriptEnabled = false;
     private boolean mAllowUniversalAccessFromFileURLs = false;
     private boolean mAllowFileAccessFromFileURLs = false;
@@ -723,6 +724,8 @@
 
     /**
      * Tell the WebView to load image resources automatically.
+     * Note that setting this flag to false this does not block image loads
+     * from WebCore cache.
      * @param flag True if the WebView should load images automatically.
      */
     public void setLoadsImagesAutomatically(boolean flag) {
@@ -747,6 +750,34 @@
     }
 
     /**
+     * Sets whether images are enabled for this WebView. Setting this from
+     * false to true will reload the blocked images in place.
+     * Note that unlike {@link #setLoadsImagesAutomatically}, setting this
+     * flag to false this will block image loads from WebCore cache as well.
+     * The default is true.
+     * @param flag whether the WebView should enable images.
+     */
+    public void setImagesEnabled(boolean flag) {
+        assert mCanModifySettings;
+        synchronized (mContentSettingsLock) {
+            if (mImagesEnabled != flag) {
+                mImagesEnabled = flag;
+                mEventHandler.syncSettingsLocked();
+            }
+        }
+    }
+
+    /**
+     * Gets whether images are enabled for this WebView.
+     * @return true if the WebView has images eanbled
+     */
+    public boolean getImagesEnabled() {
+        synchronized (mContentSettingsLock) {
+            return mImagesEnabled;
+        }
+    }
+
+    /**
      * Return true if JavaScript is enabled. <b>Note: The default is false.</b>
      *
      * @return True if JavaScript is enabled.