Bug 1170725 - Click-to-play images r?margaret draft
authorJonathan Almeida [:jonalmeida] <jalmeida@mozilla.com>
Mon, 03 Aug 2015 23:34:39 -0700
changeset 284727 c6752cd9d79ad5c04dffab2f27ecc059f3807bc3
parent 284166 7a19194812eb767bee7cdf8fc36ba9a383c1bead
child 508279 7721810c3b2d8da374e3fba1cfc4a417625f62da
push id4328
push userjalmeida@mozilla.com
push dateWed, 12 Aug 2015 20:47:54 +0000
reviewersmargaret
bugs1170725
milestone42.0a1
Bug 1170725 - Click-to-play images r?margaret This is a WIP patch that doesn't initalize the ImageBlockingPolicy.js class as expected. The init and uninit methods are commented out since the initializing is now done in the MobileComponents.manifest. I think there might be a problem there which is the cause. The assumptions I've made is that I don't need to add a window watcher, listening for the "domwindowopened" event.
mobile/android/chrome/content/browser.js
mobile/android/components/ImageBlockingPolicy.js
mobile/android/components/MobileComponents.manifest
mobile/android/components/moz.build
mobile/android/installer/package-manifest.in
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -906,17 +906,17 @@ var BrowserApp = {
       });
 
     NativeWindow.contextmenus.add(stringGetter("contextmenu.mute"),
       NativeWindow.contextmenus.mediaContext("media-unmuted"),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute");
         aTarget.muted = true;
       });
-  
+
     NativeWindow.contextmenus.add(stringGetter("contextmenu.unmute"),
       NativeWindow.contextmenus.mediaContext("media-muted"),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute");
         aTarget.muted = false;
       });
 
     NativeWindow.contextmenus.add(stringGetter("contextmenu.copyImageLocation"),
@@ -993,16 +993,24 @@ var BrowserApp = {
         let filePickerTitleKey = (aTarget instanceof HTMLVideoElement &&
                                   (aTarget.videoWidth != 0 && aTarget.videoHeight != 0))
                                   ? "SaveVideoTitle" : "SaveAudioTitle";
         // Skipped trying to pull MIME type out of cache for now
         ContentAreaUtils.internalSave(url, null, null, null, null, false,
                                       filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
                                       aTarget.ownerDocument, true, null);
       });
+
+    // TODO: Add string resource for "Show Image"
+    NativeWindow.contextmenus.add("Show Image",
+      NativeWindow.contextmenus.imageSaveableContext,
+      function(target) {
+        target.setAttribute("data-ctv-show", "true");
+        target.setAttribute("src", target.getAttribute("data-ctv-src"));
+    });
   },
 
   onAppUpdated: function() {
     // initialize the form history and passwords databases on upgrades
     Services.obs.notifyObservers(null, "FormHistory:Init", "");
     Services.obs.notifyObservers(null, "Passwords:Init", "");
 
     if (this._startupStatus === "upgrade") {
@@ -2321,17 +2329,17 @@ var NativeWindow = {
       Messaging.sendRequest({ type: "Menu:Remove", id: aId });
     },
 
     update: function(aId, aOptions) {
       if (!aOptions)
         return;
 
       Messaging.sendRequest({
-        type: "Menu:Update", 
+        type: "Menu:Update",
         id: aId,
         options: aOptions
       });
     }
   },
 
   doorhanger: {
     _callbacks: {},
@@ -2571,16 +2579,29 @@ var NativeWindow = {
 
     mediaSaveableContext: {
       matches: function mediaSaveableContextMatches(aElement) {
         return (aElement instanceof HTMLVideoElement ||
                aElement instanceof HTMLAudioElement);
       }
     },
 
+    imageBlockingPolicyContext: {
+      matches: function imageBlockingPolicyContextMatches(aElement) {
+        if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
+          // Only show the menuitem if we are blocking the image
+          if (aElement.getAttribute("data-ctv-show") == "true") {
+            return false;
+          }
+          return true;
+        }
+          return false;
+        }
+    },
+
     mediaContext: function(aMode) {
       return {
         matches: function(aElt) {
           if (aElt instanceof Ci.nsIDOMHTMLMediaElement) {
             let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE;
             if (hasError)
               return false;
 
@@ -3709,17 +3730,17 @@ Tab.prototype = {
 
     // We add in a bit of fudge just so that the end characters
     // don't accidentally get clipped. 15px is an arbitrary choice.
     gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
                                 reflozTimeout,
                                 viewportWidth - 15);
   },
 
-  /** 
+  /**
    * Reloads the tab with the desktop mode setting.
    */
   reloadWithMode: function (aDesktopMode) {
     // notify desktopmode for PIDOMWindow
     let win = this.browser.contentWindow;
     let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
     dwi.setDesktopModeViewport(aDesktopMode);
 
@@ -7215,17 +7236,17 @@ var IdentityHandler = {
    * Determine the identity of the page being displayed by examining its SSL cert
    * (if available). Return the data needed to update the UI.
    */
   checkIdentity: function checkIdentity(aState, aBrowser) {
     this._lastStatus = aBrowser.securityUI
                                .QueryInterface(Components.interfaces.nsISSLStatusProvider)
                                .SSLStatus;
 
-    // Don't pass in the actual location object, since it can cause us to 
+    // Don't pass in the actual location object, since it can cause us to
     // hold on to the window object too long.  Just pass in the fields we
     // care about. (bug 424829)
     let locationObj = {};
     try {
       let location = aBrowser.contentWindow.location;
       locationObj.host = location.host;
       locationObj.hostname = location.hostname;
       locationObj.port = location.port;
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/ImageBlockingPolicy.js
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+/**
+ * Content policy for blocking images
+ */
+
+// SVG placeholder image for blocked image content
+let DATA_IMG = "data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI%2BDQoNCjxzdmcgdmVyc2lvbj0iMS4xIg0KICAgICB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciDQogICAgIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIg0KICAgICB4PSIwIg0KICAgICB5PSIwIg0KICAgICB3aWR0aD0iMzIiDQogICAgIGhlaWdodD0iMzIiDQogICAgIHZpZXdCb3g9IjAgMCAzMiAzMiI%2BDQoNCiAgPGRlZnM%2BDQogICAgDQogICAgPG1hc2sgaWQ9Im1hc2stY3V0b3V0LWJsb2NrZWQtc2lnbiI%2BDQogICAgICA8cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9IiNmZmYiIC8%2BDQogICAgICA8Y2lyY2xlIGN4PSIyNSIgY3k9IjI1IiByPSI4IiBmaWxsPSIjMDAwIiAvPg0KICAgIDwvbWFzaz4NCg0KICAgIDxtYXNrIGlkPSJtYXNrLWN1dG91dC1mcmFtZSI%2BDQogICAgICA8cmVjdCB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIGZpbGw9IiMwMDAiIC8%2BDQogICAgICA8cmVjdCB4PSIyIiB5PSI0IiB3aWR0aD0iMjgiIGhlaWdodD0iMjQiIHJ4PSIzIiByeT0iMyIgZmlsbD0iI2ZmZiIgLz4NCiAgICAgIDxyZWN0IHg9IjQiIHk9IjYiIHdpZHRoPSIyNCIgaGVpZ2h0PSIyMCIgcng9IjIiIHJ5PSIyIiBmaWxsPSIjMDAwIiAvPg0KICAgIDwvbWFzaz4NCiAgICANCiAgICA8bWFzayBpZD0ibWFzay1jdXRvdXQtYmxvY2tlZC1zaWduLWlubmVyIj4NCiAgICAgIDxyZWN0IHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iI2ZmZiIgLz4NCiAgICAgIDxjaXJjbGUgY3g9IjI1IiBjeT0iMjUiIHI9IjQiIGZpbGw9IiMwMDAiIC8%2BDQogICAgPC9tYXNrPg0KDQogIDwvZGVmcz4NCg0KICA8ZyBpZD0iaWNvbi1mcmFtZSIgbWFzaz0idXJsKCNtYXNrLWN1dG91dC1ibG9ja2VkLXNpZ24pIj4NCiAgICA8cmVjdCBpZD0ic2hhcGUtYmFja2dyb3VuZCIgeD0iMiIgeT0iNCIgd2lkdGg9IjI4IiBoZWlnaHQ9IjI0IiByeD0iMyIgcnk9IjMiIGZpbGw9IiNmMGYxZjIiIC8%2BDQogICAgPHBvbHlnb24gcG9pbnRzPSIzLDI2IDExLDE2IDE4LDI2IiBmaWxsPSIjN2U3ZjgwIiAvPg0KICAgIDxwb2x5Z29uIHBvaW50cz0iMTEsMjYgMjMsMTAgMzEsMjAgMzEsMjYiIGZpbGw9IiM0YzRjNGQiIC8%2BDQogICAgPGNpcmNsZSBjeD0iOSIgY3k9IjExIiByPSIzIiBmaWxsPSIjOTc5ODk5IiAvPg0KICAgIDxyZWN0IHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgZmlsbD0iIzY1NjU2NiIgbWFzaz0idXJsKCNtYXNrLWN1dG91dC1mcmFtZSkiIC8%2BDQogIDwvZz4NCiAgPGcgaWQ9Imljb24tYmxvY2tlZC1zaWduIj4NCiAgICA8Y2lyY2xlIGN4PSIyNSIgY3k9IjI1IiByPSI2IiBmaWxsPSIjNjU2NTY2IiBtYXNrPSJ1cmwoI21hc2stY3V0b3V0LWJsb2NrZWQtc2lnbi1pbm5lcikiIC8%2BDQogICAgPGxpbmUgeDE9IjIxIiB5MT0iMjkiIHgyPSIyOSIgeTI9IjIxIiBzdHJva2U9IiM2NTY1NjYiIHN0cm9rZS13aWR0aD0iMiIgLz4NCiAgPC9nPg0KDQo8L3N2Zz4NCg%3D%3D";
+
+console.log("WEEEEEEEEEEEEEEE");
+let ImageBlockingPolicy = {
+  classDescription: "Click-To-View Image",
+  classID: Components.ID("{f55f77f9-d33d-4759-82fc-60db3ee0bb91}"),
+  contractID: "@mozilla.org/browser/blockimages-policy;1",
+  xpcom_categories: ["content-policy"],
+  enabled: true,
+
+  // init: function(enabled) {
+  //   let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+  //   registrar.registerFactory(this.classID, this.classDescription, this.contractID, this);
+
+  //   let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
+  //   for each (let category in this.xpcom_categories) {
+  //     catMan.addCategoryEntry(category, this.contractID, this.contractID, false, true);
+  //   }
+  //   this.enabled = enabled;
+  // },
+
+  // uninit: function() {
+  //   let catMan = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
+  //   for each(let category in this.xpcom_categories) {
+  //     catMan.deleteCategoryEntry(category, this.contractID, false);
+  //   }
+
+  //   // This needs to run asynchronously, see bug 753687
+  //   Services.tm.currentThread.dispatch(function() {
+  //     let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+  //     registrar.unregisterFactory(this.classID, this);
+  //   }.bind(this), Ci.nsIEventTarget.DISPATCH_NORMAL);
+  // },
+
+  setEnabled: function(enabled) {
+    this.enabled = enabled;
+  },
+
+  // nsIContentPolicy interface implementation
+  shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) {
+    console.log("HELLLLOOOOOOOOOOOOOOOOO")
+    if (!this.enabled) {
+      return Ci.nsIContentPolicy.ACCEPT;
+    }
+
+    if (contentType === Ci.nsIContentPolicy.TYPE_IMAGE || contentType === Ci.nsIContentPolicy.TYPE_IMAGESET) {
+      // Accept any non-http(s) image URLs
+      if (!contentLocation.schemeIs("http") && !contentLocation.schemeIs("https")) {
+        return Ci.nsIContentPolicy.ACCEPT;
+      }
+
+      if (node instanceof Ci.nsIDOMHTMLImageElement) {
+        dump(node.outerHTML + "\n");
+        // Accept if the user has asked to view the image
+        if (node.getAttribute("data-ctv-show") == "true") {
+          return Ci.nsIContentPolicy.ACCEPT;
+        }
+
+        setTimeout(() => {
+          // Cache the original image URL and swap in our placeholder
+          node.setAttribute("data-ctv-src", contentLocation.spec);
+          node.setAttribute("src", DATA_IMG);
+
+          // For imageset (img + srcset) the "srcset" is used even after we reset the "src" causing a loop.
+          // We are given the final image URL anyway, so it's OK to just remove the "srcset" value.
+          node.removeAttribute("srcset");
+        }, 0);
+      }
+
+      // Reject any image that is not associated with a DOM element
+      return Ci.nsIContentPolicy.REJECT;
+    }
+
+    // Accept all other content types
+    return Ci.nsIContentPolicy.ACCEPT;
+  },
+
+  shouldProcess: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) {
+    return Ci.nsIContentPolicy.ACCEPT;
+  },
+
+  // nsIFactory interface implementation
+  createInstance: function(outer, iid) {
+    if (outer) {
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(iid);
+  },
+
+  // nsISupports interface implementation
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsIFactory])
+};
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -36,16 +36,21 @@ category app-startup SessionStore servic
 
 # stylesheets
 category agent-style-sheets browser-content-stylesheet chrome://browser/skin/content.css
 
 # ContentPermissionPrompt.js
 component {C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5} ContentPermissionPrompt.js
 contract @mozilla.org/content-permission/prompt;1 {C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}
 
+# ImageBlockingPolicy.js
+component {F55F77F9-D33D-4759-82FC-60DB3EE0BB91} ImageBlockingPolicy.js
+contract @mozilla.org/browser/blockimages-policy;1 {F55F77F9-D33D-4759-82FC-60DB3EE0BB91}
+category content-policy ImageBlockingPolicy @mozilla.org/browser/blockimages-policy;1
+
 # XPIDialogService.js
 component {c1242012-27d8-477e-a0f1-0b098ffc329b} XPIDialogService.js
 contract @mozilla.org/addons/web-install-prompt;1 {c1242012-27d8-477e-a0f1-0b098ffc329b}
 
 # HelperAppDialog.js
 component {e9d277a0-268a-4ec2-bb8c-10fdf3e44611} HelperAppDialog.js
 contract @mozilla.org/helperapplauncherdialog;1 {e9d277a0-268a-4ec2-bb8c-10fdf3e44611}
 
--- a/mobile/android/components/moz.build
+++ b/mobile/android/components/moz.build
@@ -17,16 +17,17 @@ EXTRA_COMPONENTS += [
     'BlocklistPrompt.js',
     'BrowserCLH.js',
     'ColorPicker.js',
     'ContentDispatchChooser.js',
     'ContentPermissionPrompt.js',
     'DirectoryProvider.js',
     'FilePicker.js',
     'HelperAppDialog.js',
+    'ImageBlockingPolicy.js',
     'LoginManagerPrompter.js',
     'NSSDialogService.js',
     'PromptService.js',
     'SessionStore.js',
     'SiteSpecificUserAgent.js',
     'Snippets.js',
     'TabSource.js',
     'WebappsUpdateTimer.js',
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -1,13 +1,13 @@
 ; This Source Code Form is subject to the terms of the Mozilla Public
 ; License, v. 2.0. If a copy of the MPL was not distributed with this
 ; file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-; Package file for the Fennec build. 
+; Package file for the Fennec build.
 ;
 ; File format:
 ;
 ; [] designates a toplevel component. Example: [xpcom]
 ; - in front of a file specifies it to be removed from the destination
 ; * wildcard support to recursively copy the entire directory
 ; ; file comment
 ;
@@ -520,17 +520,17 @@
 @BINPATH@/@PREF_DIR@/mobile-branding.js
 @BINPATH@/@PREF_DIR@/channel-prefs.js
 @BINPATH@/ua-update.json
 @BINPATH@/greprefs.js
 @BINPATH@/defaults/autoconfig/prefcalls.js
 @BINPATH@/defaults/profile/prefs.js
 
 ; [Layout Engine Resources]
-; Style Sheets, Graphics and other Resources used by the layout engine. 
+; Style Sheets, Graphics and other Resources used by the layout engine.
 @BINPATH@/res/EditorOverride.css
 @BINPATH@/res/contenteditable.css
 @BINPATH@/res/designmode.css
 @BINPATH@/res/TopLevelImageDocument.css
 @BINPATH@/res/TopLevelVideoDocument.css
 @BINPATH@/res/table-add-column-after-active.gif
 @BINPATH@/res/table-add-column-after-hover.gif
 @BINPATH@/res/table-add-column-after.gif
@@ -624,16 +624,17 @@ bin/libfreebl_32int64_3.so
 @BINPATH@/chrome/chrome.manifest
 @BINPATH@/components/AboutRedirector.js
 @BINPATH@/components/AddonUpdateService.js
 @BINPATH@/components/BlocklistPrompt.js
 @BINPATH@/components/BrowserCLH.js
 @BINPATH@/components/ColorPicker.js
 @BINPATH@/components/ContentDispatchChooser.js
 @BINPATH@/components/ContentPermissionPrompt.js
+@BINPATH@/components/ImageBlockingPolicy.js
 @BINPATH@/components/DirectoryProvider.js
 @BINPATH@/components/FilePicker.js
 @BINPATH@/components/HelperAppDialog.js
 @BINPATH@/components/LoginManagerPrompter.js
 @BINPATH@/components/MobileComponents.manifest
 @BINPATH@/components/MobileComponents.xpt
 @BINPATH@/components/NSSDialogService.js
 @BINPATH@/components/PromptService.js
@@ -651,17 +652,17 @@ bin/libfreebl_32int64_3.so
 
 #ifdef MOZ_SAFE_BROWSING
 @BINPATH@/components/SafeBrowsing.jsm
 #endif
 @BINPATH@/components/XPIDialogService.js
 @BINPATH@/components/browsercomps.xpt
 
 #ifdef ENABLE_MARIONETTE
-@BINPATH@/chrome/marionette@JAREXT@ 
+@BINPATH@/chrome/marionette@JAREXT@
 @BINPATH@/chrome/marionette.manifest
 @BINPATH@/components/MarionetteComponents.manifest
 @BINPATH@/components/marionettecomponent.js
 #endif
 
 @BINPATH@/components/WebappsUpdateTimer.js
 @BINPATH@/components/DataStore.manifest
 @BINPATH@/components/DataStoreImpl.js