Files
tubestation/toolkit/components/extensions/extension-process-script.js
Kris Maglione a39f75a80e Bug 1470783: Migrate extensions framework to use sharedData for cross-process data. r=zombie
initialProcessData has the unfortunate side-effect of sending an entire copy
of all of its data to all content processes, and eagerly decoding it. For the
extension framework, this means that we wind up loading an entire copy of all
of our schema data, and of every extension's manifest and locale data, into
every process, even if we'll never need it.

The sharedData helper allows us to store an encoded copy of that data in a
shared memory region, and clone it into the current process only when we need
it, which can be a significant savings. For screenshots alone, it saves about
15K on locale and manifest data per content process, plus the size we save on
not copying schema data.

MozReview-Commit-ID: KkIOoLsBd99
2018-06-24 16:34:44 -07:00

520 lines
17 KiB
JavaScript

/* 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/. */
"use strict";
/**
* This script contains the minimum, skeleton content process code that we need
* in order to lazily load other extension modules when they are first
* necessary. Anything which is not likely to be needed immediately, or shortly
* after startup, in *every* browser process live outside of this file.
*/
ChromeUtils.import("resource://gre/modules/MessageChannel.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
ExtensionChild: "resource://gre/modules/ExtensionChild.jsm",
ExtensionCommon: "resource://gre/modules/ExtensionCommon.jsm",
ExtensionContent: "resource://gre/modules/ExtensionContent.jsm",
ExtensionPageChild: "resource://gre/modules/ExtensionPageChild.jsm",
});
ChromeUtils.import("resource://gre/modules/ExtensionUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionCommon.getConsole());
const {
DefaultWeakMap,
getInnerWindowID,
} = ExtensionUtils;
const {sharedData} = Services.cpmm;
function getData(extension, key = "") {
return sharedData.get(`extension/${extension.id}/${key}`);
}
// We need to avoid touching Services.appinfo here in order to prevent
// the wrong version from being cached during xpcshell test startup.
// eslint-disable-next-line mozilla/use-services
const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
const isContentProcess = appinfo.processType == appinfo.PROCESS_TYPE_CONTENT;
function tryMatchPatternSet(patterns, options) {
try {
return new MatchPatternSet(patterns, options);
} catch (e) {
Cu.reportError(e);
return new MatchPatternSet([]);
}
}
function parseScriptOptions(options, restrictSchemes = true) {
return {
allFrames: options.all_frames,
matchAboutBlank: options.match_about_blank,
frameID: options.frame_id,
runAt: options.run_at,
hasActiveTabPermission: options.hasActiveTabPermission,
matches: tryMatchPatternSet(options.matches, {restrictSchemes}),
excludeMatches: tryMatchPatternSet(options.exclude_matches || [], {restrictSchemes}),
includeGlobs: options.include_globs && options.include_globs.map(glob => new MatchGlob(glob)),
excludeGlobs: options.exclude_globs && options.exclude_globs.map(glob => new MatchGlob(glob)),
jsPaths: options.js || [],
cssPaths: options.css || [],
};
}
var extensions = new DefaultWeakMap(policy => {
let data = policy.initData;
if (data.serialize) {
// We have an actual Extension rather than serialized extension
// data, so serialize it now to make sure we have consistent inputs
// between parent and child processes.
data = data.serialize();
}
let extension = new ExtensionChild.BrowserExtensionContent(data);
extension.policy = policy;
return extension;
});
var contentScripts = new DefaultWeakMap(matcher => {
return new ExtensionContent.Script(extensions.get(matcher.extension),
matcher);
});
function getMessageManager(window) {
let docShell = window.document.docShell.QueryInterface(Ci.nsIInterfaceRequestor);
try {
return docShell.getInterface(Ci.nsIContentFrameMessageManager);
} catch (e) {
// Some windows don't support this interface (hidden window).
return null;
}
}
var DocumentManager;
var ExtensionManager;
class ExtensionGlobal {
constructor(global) {
this.global = global;
this.global.addMessageListener("Extension:SetFrameData", this);
this.frameData = null;
MessageChannel.addListener(global, "Extension:Capture", this);
MessageChannel.addListener(global, "Extension:DetectLanguage", this);
MessageChannel.addListener(global, "Extension:Execute", this);
MessageChannel.addListener(global, "WebNavigation:GetFrame", this);
MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this);
}
get messageFilterStrict() {
return {
innerWindowID: getInnerWindowID(this.global.content),
};
}
getFrameData(force = false) {
if (!this.frameData && force) {
this.frameData = this.global.sendSyncMessage("Extension:GetTabAndWindowId")[0];
}
return this.frameData;
}
async receiveMessage({target, messageName, recipient, data, name}) {
switch (name) {
case "Extension:SetFrameData":
if (this.frameData) {
Object.assign(this.frameData, data);
} else {
this.frameData = data;
}
if (data.viewType && WebExtensionPolicy.isExtensionProcess) {
ExtensionPageChild.expectViewLoad(this.global, data.viewType);
}
return;
}
switch (messageName) {
case "Extension:Capture":
return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options);
case "Extension:DetectLanguage":
return ExtensionContent.handleDetectLanguage(this.global, target);
case "Extension:Execute":
let policy = WebExtensionPolicy.getByID(recipient.extensionId);
let matcher = new WebExtensionContentScript(policy, parseScriptOptions(data.options, !policy.hasPermission("mozillaAddons")));
Object.assign(matcher, {
wantReturnValue: data.options.wantReturnValue,
removeCSS: data.options.remove_css,
cssOrigin: data.options.css_origin,
jsCode: data.options.jsCode,
});
let script = contentScripts.get(matcher);
// Add the cssCode to the script, so that it can be converted into a cached URL.
await script.addCSSCode(data.options.cssCode);
delete data.options.cssCode;
return ExtensionContent.handleExtensionExecute(this.global, target, data.options, script);
case "WebNavigation:GetFrame":
return ExtensionContent.handleWebNavigationGetFrame(this.global, data.options);
case "WebNavigation:GetAllFrames":
return ExtensionContent.handleWebNavigationGetAllFrames(this.global);
}
}
}
// Responsible for creating ExtensionContexts and injecting content
// scripts into them when new documents are created.
DocumentManager = {
globals: new Map(),
// Initialize listeners that we need regardless of whether extensions are
// enabled.
earlyInit() {
Services.obs.addObserver(this, "tab-content-frameloader-created"); // eslint-disable-line mozilla/balanced-listeners
},
// Initialize a frame script global which extension contexts may be loaded
// into.
initGlobal(global) {
this.globals.set(global, new ExtensionGlobal(global));
// eslint-disable-next-line mozilla/balanced-listeners
global.addEventListener("unload", () => {
this.globals.delete(global);
});
},
initExtension(extension) {
this.injectExtensionScripts(extension);
},
// Listeners
observe(subject, topic, data) {
if (topic == "tab-content-frameloader-created") {
this.initGlobal(subject);
}
},
// Script loading
injectExtensionScripts(extension) {
for (let window of this.enumerateWindows()) {
let runAt = {document_start: [], document_end: [], document_idle: []};
for (let script of extension.contentScripts) {
if (script.matchesWindow(window)) {
runAt[script.runAt].push(script);
}
}
let inject = matcher => contentScripts.get(matcher).injectInto(window);
let injectAll = matchers => Promise.all(matchers.map(inject));
// Intentionally using `.then` instead of `await`, we only need to
// chain injecting other scripts into *this* window, not all windows.
injectAll(runAt.document_start)
.then(() => injectAll(runAt.document_end))
.then(() => injectAll(runAt.document_idle));
}
},
/**
* Checks that all parent frames for the given withdow either have the
* same add-on ID, or are special chrome-privileged documents such as
* about:addons or developer tools panels.
*
* @param {Window} window
* The window to check.
* @param {string} addonId
* The add-on ID to check.
* @returns {boolean}
*/
checkParentFrames(window, addonId) {
while (window.parent !== window) {
window = window.parent;
let principal = window.document.nodePrincipal;
if (Services.scriptSecurityManager.isSystemPrincipal(principal)) {
// The add-on manager is a special case, since it contains extension
// options pages in same-type <browser> frames.
if (window.location.href === "about:addons") {
return true;
}
}
if (principal.addonId !== addonId) {
return false;
}
}
return true;
},
loadInto(policy, window) {
let extension = extensions.get(policy);
if (WebExtensionPolicy.isExtensionProcess && this.checkParentFrames(window, policy.id)) {
// We're in a top-level extension frame, or a sub-frame thereof,
// in the extension process. Inject the full extension page API.
ExtensionPageChild.initExtensionContext(extension, window);
} else {
// We're in a content sub-frame or not in the extension process.
// Only inject a minimal content script API.
ExtensionContent.initExtensionContext(extension, window);
}
},
// Helpers
* enumerateWindows(docShell) {
if (docShell) {
let enum_ = docShell.getDocShellEnumerator(docShell.typeContent,
docShell.ENUMERATE_FORWARDS);
for (let docShell of XPCOMUtils.IterSimpleEnumerator(enum_, Ci.nsIInterfaceRequestor)) {
yield docShell.getInterface(Ci.nsIDOMWindow);
}
} else {
for (let global of this.globals.keys()) {
yield* this.enumerateWindows(global.docShell);
}
}
},
};
ExtensionManager = {
// WeakMap<WebExtensionPolicy, Map<string, WebExtensionContentScript>>
registeredContentScripts: new DefaultWeakMap((extension) => new Map()),
init() {
MessageChannel.setupMessageManagers([Services.cpmm]);
Services.cpmm.addMessageListener("Extension:Startup", this);
Services.cpmm.addMessageListener("Extension:Shutdown", this);
Services.cpmm.addMessageListener("Extension:FlushJarCache", this);
Services.cpmm.addMessageListener("Extension:RegisterContentScript", this);
Services.cpmm.addMessageListener("Extension:UnregisterContentScripts", this);
for (let id of sharedData.get("extensions/activeIDs") || []) {
this.initExtension(getData({id}));
}
},
initExtensionPolicy(extension) {
let policy = WebExtensionPolicy.getByID(extension.id);
if (!policy) {
let localizeCallback, allowedOrigins, webAccessibleResources;
let restrictSchemes = !extension.permissions.has("mozillaAddons");
if (extension.localize) {
// We have a real Extension object.
localizeCallback = extension.localize.bind(extension);
allowedOrigins = extension.whiteListedHosts;
webAccessibleResources = extension.webAccessibleResources;
} else {
// We have serialized extension data;
localizeCallback = str => extensions.get(policy).localize(str);
allowedOrigins = new MatchPatternSet(extension.whiteListedHosts, {restrictSchemes});
webAccessibleResources = extension.webAccessibleResources.map(host => new MatchGlob(host));
}
let {backgroundScripts} = extension;
if (!backgroundScripts && WebExtensionPolicy.isExtensionProcess) {
({backgroundScripts} = getData(extension, "extendedData") || {});
}
policy = new WebExtensionPolicy({
id: extension.id,
mozExtensionHostname: extension.uuid,
name: extension.name,
baseURL: extension.resourceURL,
permissions: Array.from(extension.permissions),
allowedOrigins,
webAccessibleResources,
contentSecurityPolicy: extension.contentSecurityPolicy,
localizeCallback,
backgroundScripts,
contentScripts: extension.contentScripts.map(script => parseScriptOptions(script, restrictSchemes)),
});
policy.debugName = `${JSON.stringify(policy.name)} (ID: ${policy.id}, ${policy.getURL()})`;
// Register any existent dynamically registered content script for the extension
// when a content process is started for the first time (which also cover
// a content process that crashed and it has been recreated).
const registeredContentScripts = this.registeredContentScripts.get(policy);
for (let [scriptId, options] of getData(extension, "contentScripts") || []) {
const parsedOptions = parseScriptOptions(options, restrictSchemes);
const script = new WebExtensionContentScript(policy, parsedOptions);
policy.registerContentScript(script);
registeredContentScripts.set(scriptId, script);
}
policy.active = true;
policy.initData = extension;
}
return policy;
},
initExtension(data) {
if (typeof data === "string") {
data = getData({id: data});
}
let policy = this.initExtensionPolicy(data);
DocumentManager.initExtension(policy);
},
receiveMessage({name, data}) {
switch (name) {
case "Extension:Startup": {
this.initExtension(data);
Services.cpmm.sendAsyncMessage("Extension:StartupComplete");
break;
}
case "Extension:Shutdown": {
let policy = WebExtensionPolicy.getByID(data.id);
if (policy) {
if (extensions.has(policy)) {
extensions.get(policy).shutdown();
}
if (isContentProcess) {
policy.active = false;
}
}
Services.cpmm.sendAsyncMessage("Extension:ShutdownComplete");
break;
}
case "Extension:FlushJarCache": {
ExtensionUtils.flushJarCache(data.path);
Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete");
break;
}
case "Extension:RegisterContentScript": {
let policy = WebExtensionPolicy.getByID(data.id);
if (policy) {
const registeredContentScripts = this.registeredContentScripts.get(policy);
if (registeredContentScripts.has(data.scriptId)) {
Cu.reportError(new Error(
`Registering content script ${data.scriptId} on ${data.id} more than once`));
} else {
try {
const parsedOptions = parseScriptOptions(data.options, !policy.hasPermission("mozillaAddons"));
const script = new WebExtensionContentScript(policy, parsedOptions);
policy.registerContentScript(script);
registeredContentScripts.set(data.scriptId, script);
} catch (e) {
Cu.reportError(e);
}
}
}
Services.cpmm.sendAsyncMessage("Extension:RegisterContentScriptComplete");
break;
}
case "Extension:UnregisterContentScripts": {
let policy = WebExtensionPolicy.getByID(data.id);
if (policy) {
const registeredContentScripts = this.registeredContentScripts.get(policy);
for (const scriptId of data.scriptIds) {
const script = registeredContentScripts.get(scriptId);
if (script) {
try {
policy.unregisterContentScript(script);
registeredContentScripts.delete(scriptId);
} catch (e) {
Cu.reportError(e);
}
}
}
}
Services.cpmm.sendAsyncMessage("Extension:UnregisterContentScriptsComplete");
break;
}
}
},
};
function ExtensionProcessScript() {
if (!ExtensionProcessScript.singleton) {
ExtensionProcessScript.singleton = this;
}
return ExtensionProcessScript.singleton;
}
ExtensionProcessScript.singleton = null;
ExtensionProcessScript.prototype = {
classID: Components.ID("{21f9819e-4cdf-49f9-85a0-850af91a5058}"),
QueryInterface: ChromeUtils.generateQI([Ci.mozIExtensionProcessScript]),
get wrappedJSObject() { return this; },
getFrameData(global, force) {
let extGlobal = DocumentManager.globals.get(global);
return extGlobal && extGlobal.getFrameData(force);
},
initExtension(extension) {
return ExtensionManager.initExtensionPolicy(extension);
},
initExtensionDocument(policy, doc) {
if (DocumentManager.globals.has(getMessageManager(doc.defaultView))) {
DocumentManager.loadInto(policy, doc.defaultView);
}
},
getExtensionChild(id) {
let policy = WebExtensionPolicy.getByID(id);
if (policy) {
return extensions.get(policy);
}
},
preloadContentScript(contentScript) {
contentScripts.get(contentScript).preload();
},
loadContentScript(contentScript, window) {
if (DocumentManager.globals.has(getMessageManager(window))) {
contentScripts.get(contentScript).injectInto(window);
}
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ExtensionProcessScript]);
DocumentManager.earlyInit();
ExtensionManager.init();