Bug 1247455 - Add a .removeCSS method to complement .insertCSS. r=kmag
authorClaas Augner <mozilla@caugner.de>
Fri, 15 Apr 2016 00:39:09 +0200
changeset 338251 be67702cf86eda352626dc73d0f59308d479a333
parent 338250 0d9cbc1fdbe9baac069a21caec3c98a239a8f331
child 338252 981fc919965e7f3276afe6fb882f6f19ad35bb36
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1247455
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1247455 - Add a .removeCSS method to complement .insertCSS. r=kmag
browser/components/extensions/ext-tabs.js
browser/components/extensions/schemas/tabs.json
browser/components/extensions/test/browser/browser.ini
browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js
toolkit/components/extensions/ExtensionContent.jsm
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -702,23 +702,25 @@ extensions.registerSchemaAPI("tabs", nul
 
         let browser = tab.linkedBrowser;
         let recipient = {innerWindowID: browser.innerWindowID};
 
         return context.sendMessage(browser.messageManager, "Extension:DetectLanguage",
                                    {}, {recipient});
       },
 
+      // Used to executeScript, insertCSS and removeCSS.
       _execute: function(tabId, details, kind, method) {
         let tab = tabId !== null ? TabManager.getTab(tabId) : TabManager.activeTab;
         let mm = tab.linkedBrowser.messageManager;
 
         let options = {
           js: [],
           css: [],
+          remove_css: method == "removeCSS",
         };
 
         // We require a `code` or a `file` property, but we can't accept both.
         if ((details.code === null) == (details.file === null)) {
           return Promise.reject({message: `${method} requires either a 'code' or a 'file' property, but not both`});
         }
 
         if (details.frameId !== null && details.allFrames) {
@@ -768,16 +770,20 @@ extensions.registerSchemaAPI("tabs", nul
       executeScript: function(tabId, details) {
         return self.tabs._execute(tabId, details, "js", "executeScript");
       },
 
       insertCSS: function(tabId, details) {
         return self.tabs._execute(tabId, details, "css", "insertCSS");
       },
 
+      removeCSS: function(tabId, details) {
+        return self.tabs._execute(tabId, details, "css", "removeCSS");
+      },
+
       connect: function(tabId, connectInfo) {
         let tab = TabManager.getTab(tabId);
         let mm = tab.linkedBrowser.messageManager;
 
         let name = "";
         if (connectInfo && connectInfo.name !== null) {
           name = connectInfo.name;
         }
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -862,16 +862,43 @@
             "name": "callback",
             "optional": true,
             "description": "Called when all the CSS has been inserted.",
             "parameters": []
           }
         ]
       },
       {
+        "name": "removeCSS",
+        "type": "function",
+        "description": "Removes injected CSS from a page. For details, see the $(topic:content_scripts)[programmatic injection] section of the content scripts doc.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "integer",
+            "name": "tabId",
+            "minimum": 0,
+            "optional": true,
+            "description": "The ID of the tab from which to remove the injected CSS; defaults to the active tab of the current window."
+          },
+          {
+            "$ref": "extensionTypes.InjectDetails",
+            "name": "details",
+            "description": "Details of the CSS text to remove."
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "description": "Called when all the CSS has been removed.",
+            "parameters": []
+          }
+        ]
+      },
+      {
         "name": "setZoom",
         "type": "function",
         "description": "Zooms a specified tab.",
         "async": "callback",
         "parameters": [
           {
             "type": "integer",
             "name": "tabId",
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -48,16 +48,17 @@ support-files =
 [browser_ext_tabs_duplicate.js]
 [browser_ext_tabs_events.js]
 [browser_ext_tabs_executeScript.js]
 [browser_ext_tabs_executeScript_good.js]
 [browser_ext_tabs_executeScript_bad.js]
 [browser_ext_tabs_executeScript_runAt.js]
 [browser_ext_tabs_getCurrent.js]
 [browser_ext_tabs_insertCSS.js]
+[browser_ext_tabs_removeCSS.js]
 [browser_ext_tabs_move.js]
 [browser_ext_tabs_move_window.js]
 [browser_ext_tabs_move_window_multiple.js]
 [browser_ext_tabs_move_window_pinned.js]
 [browser_ext_tabs_onHighlighted.js]
 [browser_ext_tabs_onUpdated.js]
 [browser_ext_tabs_query.js]
 [browser_ext_tabs_reload.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_removeCSS.js
@@ -0,0 +1,103 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* testExecuteScript() {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/", true);
+
+  function background() {
+    let promises = [
+      // Insert CSS file.
+      {
+        background: "transparent",
+        foreground: "rgb(0, 113, 4)",
+        promise: () => {
+          return browser.tabs.insertCSS({
+            file: "file2.css",
+          });
+        },
+      },
+      // Insert CSS code.
+      {
+        background: "rgb(42, 42, 42)",
+        foreground: "rgb(0, 113, 4)",
+        promise: () => {
+          return browser.tabs.insertCSS({
+            code: "* { background: rgb(42, 42, 42) }",
+          });
+        },
+      },
+      // Remove CSS code again.
+      {
+        background: "transparent",
+        foreground: "rgb(0, 113, 4)",
+        promise: () => {
+          return browser.tabs.removeCSS({
+            code: "* { background: rgb(42, 42, 42) }",
+          });
+        },
+      },
+      // Remove CSS file again.
+      {
+        background: "transparent",
+        foreground: "rgb(0, 0, 0)",
+        promise: () => {
+          return browser.tabs.removeCSS({
+            file: "file2.css",
+          });
+        },
+      },
+    ];
+
+    function checkCSS() {
+      let computedStyle = window.getComputedStyle(document.body);
+      return [computedStyle.backgroundColor, computedStyle.color];
+    }
+
+    function next() {
+      if (!promises.length) {
+        return;
+      }
+
+      let {promise, background, foreground} = promises.shift();
+      return promise().then(result => {
+        browser.test.assertEq(undefined, result, "Expected callback result");
+
+        return browser.tabs.executeScript({
+          code: `(${checkCSS})()`,
+        });
+      }).then(result => {
+        browser.test.assertEq(background, result[0], "Expected background color");
+        browser.test.assertEq(foreground, result[1], "Expected foreground color");
+        return next();
+      });
+    }
+
+    next().then(() => {
+      browser.test.notifyPass("removeCSS");
+    }).catch(e => {
+      browser.test.fail(`Error: ${e} :: ${e.stack}`);
+      browser.test.notifyFailure("removeCSS");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "permissions": ["http://mochi.test/"],
+    },
+
+    background,
+
+    files: {
+      "file2.css": "* { color: rgb(0, 113, 4) }",
+    },
+  });
+
+  yield extension.startup();
+
+  yield extension.awaitFinish("removeCSS");
+
+  yield extension.unload();
+
+  yield BrowserTestUtils.removeTab(tab);
+});
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -150,16 +150,17 @@ var api = context => {
 };
 
 // Represents a content script.
 function Script(options, deferred = PromiseUtils.defer()) {
   this.options = options;
   this.run_at = this.options.run_at;
   this.js = this.options.js || [];
   this.css = this.options.css || [];
+  this.remove_css = this.options.remove_css;
 
   this.deferred = deferred;
 
   this.matches_ = new MatchPattern(this.options.matches);
   this.exclude_matches_ = new MatchPattern(this.options.exclude_matches || null);
   // TODO: MatchPattern should pre-mangle host-only patterns so that we
   // don't need to call a separate match function.
   this.matches_host_ = new MatchPattern(this.options.matchesHost || null);
@@ -206,25 +207,35 @@ Script.prototype = {
       this.deferred.reject({message: "No matching window"});
       return;
     }
 
     if (shouldRun("document_start")) {
       let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
                            .getInterface(Ci.nsIDOMWindowUtils);
 
-      for (let url of this.css) {
-        url = extension.baseURI.resolve(url);
-        runSafeSyncWithoutClone(winUtils.loadSheetUsingURIString, url, winUtils.AUTHOR_SHEET);
-        this.deferred.resolve();
+      // We can handle CSS urls (css) and CSS code (cssCode).
+      let cssUrls = [];
+      for (let cssUrl of this.css) {
+        cssUrl = extension.baseURI.resolve(cssUrl);
+        cssUrls.push(cssUrl);
       }
 
       if (this.options.cssCode) {
-        let url = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
-        runSafeSyncWithoutClone(winUtils.loadSheetUsingURIString, url, winUtils.AUTHOR_SHEET);
+        let cssUrl = "data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode);
+        cssUrls.push(cssUrl);
+      }
+
+      // We can insertCSS and removeCSS.
+      let method = this.remove_css ? winUtils.removeSheetUsingURIString : winUtils.loadSheetUsingURIString;
+      for (let cssUrl of cssUrls) {
+        runSafeSyncWithoutClone(method, cssUrl, winUtils.AUTHOR_SHEET);
+      }
+
+      if (cssUrls.length > 0) {
         this.deferred.resolve();
       }
     }
 
     let result;
     let scheduled = this.run_at || "document_idle";
     if (shouldRun(scheduled)) {
       for (let url of this.js) {
@@ -543,16 +554,17 @@ DocumentManager = {
 
     if (event.type == "DOMContentLoaded") {
       this.trigger("document_end", window);
     } else if (event.type == "load") {
       this.trigger("document_idle", window);
     }
   },
 
+  // Used to executeScript, insertCSS and removeCSS.
   executeScript(global, extensionId, options) {
     let executeInWin = (window) => {
       let deferred = PromiseUtils.defer();
       let script = new Script(options, deferred);
 
       if (script.matches(window)) {
         let context = this.getContentScriptContext(extensionId, window);
         context.addScript(script);
@@ -876,16 +888,17 @@ class ExtensionGlobal {
 
       let encoding = doc.characterSet;
 
       return LanguageDetector.detectLanguage({language, tld, text, encoding})
                              .then(result => result.language);
     });
   }
 
+  // Used to executeScript, insertCSS and removeCSS.
   handleExtensionExecute(target, extensionId, options) {
     return DocumentManager.executeScript(target, extensionId, options).then(result => {
       try {
         // Make sure we can structured-clone the result value before
         // we try to send it back over the message manager.
         Cu.cloneInto(result, target);
       } catch (e) {
         return Promise.reject({message: "Script returned non-structured-clonable data"});