Bug 1333990: Part 3a - Use async loading and in-memory caching for WebExtension content scripts. r?aswan
MozReview-Commit-ID: GcdKDbWcUtu
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -44,16 +44,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
+ DefaultMap,
EventEmitter,
LocaleData,
defineLazyGetter,
flushJarCache,
getInnerWindowID,
promiseDocumentReady,
runSafeSyncWithoutClone,
} = ExtensionUtils;
@@ -97,41 +98,57 @@ var apiManager = new class extends Schem
registerSchemaAPI(namespace, envType, getAPI) {
if (envType == "content_child") {
super.registerSchemaAPI(namespace, envType, getAPI);
}
}
}();
+class ScriptCache extends DefaultMap {
+ constructor(options) {
+ super(url => ChromeUtils.compileScript(url, options));
+ }
+}
+
// 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.css_origin = this.options.css_origin;
this.deferred = deferred;
+ this.scriptCache = extension[options.wantReturnValue ? "dynamicScripts"
+ : "staticScripts"];
+ if (options.wantReturnValue) {
+ this.compileScripts();
+ }
+
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);
this.include_globs_ = new MatchGlobs(this.options.include_globs);
this.exclude_globs_ = new MatchGlobs(this.options.exclude_globs);
this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode);
}
Script.prototype = {
+ compileScripts() {
+ return this.js.map(url => this.scriptCache.get(url));
+ },
+
get cssURLs() {
// We can handle CSS urls (css) and CSS code (cssCode).
let urls = [];
for (let url of this.css) {
urls.push(this.extension.baseURI.resolve(url));
}
if (this.options.cssCode) {
@@ -252,47 +269,42 @@ Script.prototype = {
for (let url of cssURLs) {
runSafeSyncWithoutClone(method, url, type);
}
this.deferred.resolve();
}
}
- let result;
let scheduled = this.run_at || "document_idle";
if (shouldRun(scheduled)) {
- for (let url of this.js) {
- url = this.extension.baseURI.resolve(url);
+ let scriptsPromise = Promise.all(this.compileScripts());
- let options = {
- target: sandbox,
- charset: "UTF-8",
- // Inject asynchronously unless we're expected to inject before any
- // page scripts have run, and we haven't already missed that boat.
- async: this.run_at !== "document_start" || when !== "document_start",
- };
- try {
- result = Services.scriptloader.loadSubScriptWithOptions(url, options);
- } catch (e) {
- Cu.reportError(e);
- this.deferred.reject(e);
- }
+ // If we're supposed to inject at the start of the document load,
+ // and we haven't already missed that point, block further parsing
+ // until the scripts have been loaded.
+ if (this.run_at === "document_start" && when === "document_start") {
+ window.document.blockParsing(scriptsPromise);
}
- if (this.options.jsCode) {
- try {
+ this.deferred.resolve(scriptsPromise.then(scripts => {
+ let result;
+
+ // The evaluations below may throw, in which case the promise will be
+ // automatically rejected.
+ for (let script of scripts) {
+ result = script.executeInGlobal(sandbox);
+ }
+
+ if (this.options.jsCode) {
result = Cu.evalInSandbox(this.options.jsCode, sandbox, "latest");
- } catch (e) {
- Cu.reportError(e);
- this.deferred.reject(e);
}
- }
- this.deferred.resolve(result);
+ return result;
+ }));
}
},
};
function getWindowMessageManager(contentWindow) {
let ir = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIInterfaceRequestor);
@@ -790,17 +802,20 @@ class BrowserExtensionContent extends Ev
this.id = data.id;
this.uuid = data.uuid;
this.data = data;
this.instanceId = data.instanceId;
this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
- this.scripts = data.content_scripts.map(scriptData => new Script(this, scriptData));
+ defineLazyGetter(this, "scripts", () => {
+ return data.content_scripts.map(scriptData => new Script(this, scriptData));
+ });
+
this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
this.permissions = data.permissions;
this.principal = data.principal;
this.localeData = new LocaleData(data.localeData);
this.manifest = data.manifest;
@@ -852,16 +867,24 @@ class BrowserExtensionContent extends Ev
let match = /^manifest:(.*)/.exec(perm);
if (match) {
return this.manifest[match[1]] != null;
}
return this.permissions.has(perm);
}
}
+defineLazyGetter(BrowserExtensionContent.prototype, "staticScripts", () => {
+ return new ScriptCache({returnValue: false});
+});
+
+defineLazyGetter(BrowserExtensionContent.prototype, "dynamicScripts", () => {
+ return new ScriptCache({returnValue: true});
+});
+
ExtensionManager = {
// Map[extensionId, BrowserExtensionContent]
extensions: new Map(),
init() {
Schemas.init();
ExtensionChild.initOnce();
--- a/toolkit/components/extensions/ExtensionTabs.jsm
+++ b/toolkit/components/extensions/ExtensionTabs.jsm
@@ -558,16 +558,18 @@ class TabBase {
options.run_at = "document_idle";
}
if (details.cssOrigin !== null) {
options.css_origin = details.cssOrigin;
} else {
options.css_origin = "author";
}
+ options.wantReturnValue = true;
+
return this.sendMessage(context, "Extension:Execute", {options});
}
/**
* Executes a script in the tab's content window, and returns a Promise which
* resolves to the result of the evaluation, or rejects to the value of any
* error the injection generates.
*
--- a/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_web_accessible_resources.html
@@ -213,17 +213,17 @@ add_task(function* test_web_accessible_r
function testScript() {
window.postMessage("test-script-loaded", "*");
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"content_scripts": [{
"matches": ["http://example.com/*/file_csp.html"],
- "run_at": "document_start",
+ "run_at": "document_end",
"js": ["content_script_helper.js", "content_script.js"],
}],
"web_accessible_resources": [
"image.png",
"test_script.js",
],
},
background,
@@ -308,17 +308,17 @@ add_task(function* test_web_accessible_r
function testScript() {
window.postMessage("accessible-script-loaded", "*");
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"content_scripts": [{
"matches": ["https://example.com/*/file_mixed.html"],
- "run_at": "document_start",
+ "run_at": "document_end",
"js": ["content_script_helper.js", "content_script.js"],
}],
"web_accessible_resources": [
"image.png",
"test_script.js",
],
},
background,