bug 1272890 - implement match_about_blank for content scripts r?kmag
MozReview-Commit-ID: 3iZLpUw5LF4
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -96,16 +96,17 @@ var apiManager = new class extends Schem
// Represents a content script.
function Script(extension, options, deferred = PromiseUtils.defer()) {
this.extension = extension;
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.match_about_blank = this.options.match_about_blank;
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);
@@ -135,16 +136,22 @@ Script.prototype = {
let uri = window.document.documentURIObject;
// If mozAddonManager is present on this page, don't allow
// content scripts.
if (window.navigator.mozAddonManager !== undefined) {
return false;
}
+ if (this.match_about_blank && ["about:blank", "about:srcdoc"].includes(uri.spec)) {
+ // When matching about:blank/srcdoc documents, the checks below
+ // need to be performed against the "owner" document's URI.
+ uri = window.document.nodePrincipal.URI;
+ }
+
if (!(this.matches_.matches(uri) || this.matches_host_.matchesIgnoringPath(uri))) {
return false;
}
if (this.exclude_matches_.matches(uri)) {
return false;
}
@@ -161,18 +168,16 @@ Script.prototype = {
if (this.options.frame_id != null) {
if (WebNavigationFrames.getFrameId(window) != this.options.frame_id) {
return false;
}
} else if (!this.options.all_frames && window.top != window) {
return false;
}
- // TODO: match_about_blank.
-
return true;
},
cleanup(window) {
if (!this.remove_css) {
let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
@@ -421,73 +426,91 @@ DocumentManager = {
// Map[windowId -> Map[extensionId -> ExtensionContext]]
contentScriptWindows: new Map(),
// Map[windowId -> ExtensionContext]
extensionPageWindows: new Map(),
init() {
+ Services.obs.addObserver(this, "content-document-global-created", false);
Services.obs.addObserver(this, "document-element-inserted", false);
Services.obs.addObserver(this, "inner-window-destroyed", false);
},
uninit() {
+ Services.obs.removeObserver(this, "content-document-global-created");
Services.obs.removeObserver(this, "document-element-inserted");
Services.obs.removeObserver(this, "inner-window-destroyed");
},
getWindowState(contentWindow) {
let readyState = contentWindow.document.readyState;
if (readyState == "complete") {
return "document_idle";
}
if (readyState == "interactive") {
return "document_end";
}
return "document_start";
},
+ loadInto(window) {
+ // Enable the content script APIs should be available in subframes' window
+ // if it is recognized as a valid addon id (see Bug 1214658 for rationale).
+ const {
+ NO_PRIVILEGES,
+ CONTENTSCRIPT_PRIVILEGES,
+ FULL_PRIVILEGES,
+ } = ExtensionManagement.API_LEVELS;
+ let extensionId = ExtensionManagement.getAddonIdForWindow(window);
+ let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
+
+ if (apiLevel != NO_PRIVILEGES) {
+ let extension = ExtensionManager.get(extensionId);
+ if (extension) {
+ if (apiLevel == CONTENTSCRIPT_PRIVILEGES) {
+ DocumentManager.getExtensionPageContext(extension, window);
+ } else if (apiLevel == FULL_PRIVILEGES) {
+ ExtensionChild.createExtensionContext(extension, window);
+ }
+ }
+ }
+ },
+
observe: function(subject, topic, data) {
- if (topic == "document-element-inserted") {
+ // For some types of documents (about:blank), we only see the first
+ // notification, for others (data: URIs) we only observe the second.
+ if (topic == "content-document-global-created" || topic == "document-element-inserted") {
let document = subject;
let window = document && document.defaultView;
+
+ if (topic == "content-document-global-created") {
+ window = subject;
+ document = window && window.document;
+ }
+
if (!document || !document.location || !window) {
return;
}
// Make sure we only load into frames that ExtensionContent.init
// was called on (i.e., not frames for social or sidebars).
let mm = getWindowMessageManager(window);
if (!mm || !ExtensionContent.globals.has(mm)) {
return;
}
- // Enable the content script APIs should be available in subframes' window
- // if it is recognized as a valid addon id (see Bug 1214658 for rationale).
- const {
- NO_PRIVILEGES,
- CONTENTSCRIPT_PRIVILEGES,
- FULL_PRIVILEGES,
- } = ExtensionManagement.API_LEVELS;
- let extensionId = ExtensionManagement.getAddonIdForWindow(window);
- let apiLevel = ExtensionManagement.getAPILevelForWindow(window, extensionId);
-
- if (apiLevel != NO_PRIVILEGES) {
- let extension = ExtensionManager.get(extensionId);
- if (extension) {
- if (apiLevel == CONTENTSCRIPT_PRIVILEGES) {
- DocumentManager.getExtensionPageContext(extension, window);
- } else if (apiLevel == FULL_PRIVILEGES) {
- ExtensionChild.createExtensionContext(extension, window);
- }
- }
+ // Load on document-element-inserted, except for about:blank which doesn't
+ // see it, and needs special late handling on DOMContentLoaded event.
+ if (topic === "document-element-inserted") {
+ this.loadInto(window);
+ this.trigger("document_start", window);
}
- this.trigger("document_start", window);
/* eslint-disable mozilla/balanced-listeners */
window.addEventListener("DOMContentLoaded", this, true);
window.addEventListener("load", this, true);
/* eslint-enable mozilla/balanced-listeners */
} else if (topic == "inner-window-destroyed") {
let windowId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
MessageChannel.abortResponses({innerWindowID: windowId});
@@ -521,16 +544,22 @@ DocumentManager = {
// listening on.
return;
}
window.removeEventListener(event.type, this, true);
// Need to check if we're still on the right page? Greasemonkey does this.
if (event.type == "DOMContentLoaded") {
+ // By this time, we can be sure if this is an explicit about:blank
+ // document, and if it needs special late loading and fake trigger.
+ if (window.location.href === "about:blank") {
+ this.loadInto(window);
+ this.trigger("document_start", window);
+ }
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) {
@@ -662,31 +691,29 @@ DocumentManager = {
this.extensionCount--;
if (this.extensionCount == 0) {
this.uninit();
}
},
trigger(when, window) {
- let state = this.getWindowState(window);
-
- if (state == "document_start") {
+ if (when === "document_start") {
for (let extension of ExtensionManager.extensions.values()) {
for (let script of extension.scripts) {
if (script.matches(window)) {
let context = this.getContentScriptContext(extension, window);
context.addScript(script);
}
}
}
} else {
let contexts = this.contentScriptWindows.get(getInnerWindowID(window)) || new Map();
for (let context of contexts.values()) {
- context.triggerScripts(state);
+ context.triggerScripts(this.getWindowState(window));
}
}
},
};
// Represents a browser extension in the content process.
function BrowserExtensionContent(data) {
this.id = data.id;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_with_about_blank.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+ <iframe id="a_b" src="about:blank"></iframe>
+ <iframe srcdoc="galactica actual" src="adama"></iframe>
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -13,16 +13,17 @@ support-files =
file_webNavigation_frameClientRedirect.html
file_webNavigation_frameRedirect.html
file_webNavigation_manualSubframe.html
file_webNavigation_manualSubframe_page1.html
file_webNavigation_manualSubframe_page2.html
file_WebNavigation_page1.html
file_WebNavigation_page2.html
file_WebNavigation_page3.html
+ file_with_about_blank.html
file_image_good.png
file_image_bad.png
file_image_redirect.png
file_style_good.css
file_style_bad.css
file_style_redirect.css
file_script_good.js
file_script_bad.js
@@ -45,16 +46,17 @@ skip-if = os == 'android' # Android does
[test_ext_content_security_policy.html]
[test_ext_contentscript.html]
[test_ext_contentscript_api_injection.html]
[test_ext_contentscript_context.html]
[test_ext_contentscript_create_iframe.html]
[test_ext_contentscript_devtools_metadata.html]
[test_ext_contentscript_exporthelpers.html]
[test_ext_contentscript_css.html]
+[test_ext_contentscript_about_blank.html]
[test_ext_contentscript_teardown.html]
skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
[test_ext_exclude_include_globs.html]
[test_ext_i18n_css.html]
[test_ext_generate.html]
[test_ext_notifications.html]
[test_ext_permission_xhr.html]
[test_ext_runtime_connect.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_about_blank.html
@@ -0,0 +1,117 @@
+<!doctype html>
+<html>
+<head>
+ <title>Test content script match_about_blank option</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+add_task(function* test_contentscript_about_blank() {
+ const manifest = {
+ content_scripts: [
+ {
+ match_about_blank: true,
+ matches: ["http://mochi.test/*/file_with_about_blank.html", "http://example.com/*"],
+ all_frames: true,
+ css: ["all.css"],
+ js: ["all.js"],
+ }, {
+ matches: ["http://mochi.test/*/file_with_about_blank.html"],
+ css: ["mochi_without.css"],
+ js: ["mochi_without.js"],
+ all_frames: true,
+ }, {
+ match_about_blank: true,
+ matches: ["http://mochi.test/*/file_with_about_blank.html"],
+ css: ["mochi_with.css"],
+ js: ["mochi_with.js"],
+ all_frames: true,
+ },
+ ],
+ };
+
+ const files = {
+ "all.js": function() {
+ browser.runtime.sendMessage("all");
+ },
+ "all.css": `
+ body { color: red; }
+ `,
+ "mochi_without.js": function() {
+ browser.runtime.sendMessage("mochi_without");
+ },
+ "mochi_without.css": `
+ body { background: yellow; }
+ `,
+ "mochi_with.js": function() {
+ browser.runtime.sendMessage("mochi_with");
+ },
+ "mochi_with.css": `
+ body { text-align: right; }
+ `,
+ };
+
+ function background() {
+ browser.runtime.onMessage.addListener((script, {url}) => {
+ const kind = url.startsWith("about:") ? url : "top";
+ browser.test.sendMessage("script", [script, kind, url]);
+ browser.test.sendMessage(`${script}:${kind}`);
+ });
+ }
+
+ const PATH = "tests/toolkit/components/extensions/test/mochitest/file_with_about_blank.html";
+ const extension = ExtensionTestUtils.loadExtension({manifest, files, background});
+ yield extension.startup();
+
+ let count = 0;
+ extension.onMessage("script", script => {
+ info(`script ran: ${script}`);
+ count++;
+ });
+
+ let win = window.open("http://example.com/" + PATH);
+ yield Promise.all([
+ extension.awaitMessage("all:top"),
+ extension.awaitMessage("all:about:blank"),
+ extension.awaitMessage("all:about:srcdoc"),
+ ]);
+ is(count, 3, "exactly 3 scripts ran");
+ win.close();
+
+ win = window.open("http://mochi.test:8888/" + PATH);
+ yield Promise.all([
+ extension.awaitMessage("all:top"),
+ extension.awaitMessage("all:about:blank"),
+ extension.awaitMessage("all:about:srcdoc"),
+ extension.awaitMessage("mochi_without:top"),
+ extension.awaitMessage("mochi_with:top"),
+ extension.awaitMessage("mochi_with:about:blank"),
+ extension.awaitMessage("mochi_with:about:srcdoc"),
+ ]);
+
+ let style = win.getComputedStyle(win.document.body);
+ is(style.color, "rgb(255, 0, 0)", "top window text color is red");
+ is(style.backgroundColor, "rgb(255, 255, 0)", "top window background is yellow");
+ is(style.textAlign, "right", "top window text is right-aligned");
+
+ let a_b = win.document.getElementById("a_b");
+ style = a_b.contentWindow.getComputedStyle(a_b.contentDocument.body);
+ is(style.color, "rgb(255, 0, 0)", "about:blank iframe text color is red");
+ is(style.backgroundColor, "transparent", "about:blank iframe background is transparent");
+ is(style.textAlign, "right", "about:blank text is right-aligned");
+
+ is(count, 10, "exactly 7 more scripts ran");
+ win.close();
+
+ yield extension.unload();
+});
+</script>
+
+</body>
+</html>