Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
/**
* RemotePageChild is a base class for an unprivileged internal page, typically
* an about: page. A specific implementation should subclass the RemotePageChild
* actor with a more specific actor for that page. Typically, the child is not
* needed, but the parent actor will respond to messages and provide results
* directly to the page.
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AsyncPrefs: "resource://gre/modules/AsyncPrefs.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
RemotePageAccessManager:
"resource://gre/modules/RemotePageAccessManager.sys.mjs",
});
export class RemotePageChild extends JSWindowActorChild {
actorCreated() {
this.listeners = new Map();
this.exportBaseFunctions();
}
exportBaseFunctions() {
const exportableFunctions = [
"RPMSendAsyncMessage",
"RPMSendQuery",
"RPMAddMessageListener",
"RPMRemoveMessageListener",
"RPMGetIntPref",
"RPMGetStringPref",
"RPMGetBoolPref",
"RPMSetPref",
"RPMGetFormatURLPref",
"RPMIsWindowPrivate",
];
this.exportFunctions(exportableFunctions);
}
/**
* Exports a list of functions to be accessible by the privileged page.
* Subclasses may call this function to add functions that are specific
* to a page. When the page calls a function, a function with the same
* name is called within the child actor.
*
* Only functions that appear in the whitelist in the
* RemotePageAccessManager for that page will be exported.
*
* @param array of function names.
*/
exportFunctions(functions) {
let document = this.document;
let principal = document.nodePrincipal;
// If there is no content principal, don't export any functions.
if (!principal) {
return;
}
let window = this.contentWindow;
for (let fnname of functions) {
let allowAccess = lazy.RemotePageAccessManager.checkAllowAccessToFeature(
principal,
fnname,
document
);
if (allowAccess) {
// Wrap each function in an access checking function.
function accessCheckedFn(...args) {
this.checkAllowAccess(fnname, args[0]);
return this[fnname](...args);
}
Cu.exportFunction(accessCheckedFn.bind(this), window, {
defineAs: fnname,
});
}
}
}
handleEvent() {
// Do nothing. The DOMDocElementInserted event is just used to create
// the actor.
}
receiveMessage(messagedata) {
let message = {
name: messagedata.name,
data: messagedata.data,
};
let listeners = this.listeners.get(message.name);
if (!listeners) {
return;
}
let clonedMessage = Cu.cloneInto(message, this.contentWindow);
for (let listener of listeners.values()) {
try {
listener(clonedMessage);
} catch (e) {
console.error(e);
}
}
}
wrapPromise(promise) {
return new this.contentWindow.Promise((resolve, reject) =>
promise.then(resolve, reject)
);
}
/**
* Returns true if a feature cannot be accessed by the current page.
* Throws an exception if the feature may not be accessed.
* @param aDocument child process document to call from
* @param aFeature to feature to check access to
* @param aValue value that must be included with that feature's whitelist
* @returns true if access is allowed or throws an exception otherwise
*/
checkAllowAccess(aFeature, aValue) {
let doc = this.document;
if (!lazy.RemotePageAccessManager.checkAllowAccess(doc, aFeature, aValue)) {
throw new Error(
"RemotePageAccessManager does not allow access to " + aFeature
);
}
return true;
}
addPage(aUrl, aFunctionMap) {
lazy.RemotePageAccessManager.addPage(aUrl, aFunctionMap);
}
// Implementation of functions that are exported into the page.
RPMSendAsyncMessage(aName, aData = null) {
this.sendAsyncMessage(aName, aData);
}
RPMSendQuery(aName, aData = null) {
return this.wrapPromise(
new Promise(resolve => {
this.sendQuery(aName, aData).then(result => {
resolve(Cu.cloneInto(result, this.contentWindow));
});
})
);
}
/**
* Adds a listener for messages. Many callbacks can be registered for the
* same message if necessary. An attempt to register the same callback for the
* same message twice will be ignored. When called the callback is passed an
* object with these properties:
* name: The message name
* data: Any data sent with the message
*/
RPMAddMessageListener(aName, aCallback) {
if (!this.listeners.has(aName)) {
this.listeners.set(aName, new Set([aCallback]));
} else {
this.listeners.get(aName).add(aCallback);
}
}
/**
* Removes a listener for messages.
*/
RPMRemoveMessageListener(aName, aCallback) {
if (!this.listeners.has(aName)) {
return;
}
this.listeners.get(aName).delete(aCallback);
}
RPMGetIntPref(aPref, defaultValue) {
// Only call with a default value if it's defined, to be able to throw
// errors for non-existent prefs.
if (defaultValue !== undefined) {
return Services.prefs.getIntPref(aPref, defaultValue);
}
return Services.prefs.getIntPref(aPref);
}
RPMGetStringPref(aPref) {
return Services.prefs.getStringPref(aPref);
}
RPMGetBoolPref(aPref, defaultValue) {
// Only call with a default value if it's defined, to be able to throw
// errors for non-existent prefs.
if (defaultValue !== undefined) {
return Services.prefs.getBoolPref(aPref, defaultValue);
}
return Services.prefs.getBoolPref(aPref);
}
RPMSetPref(aPref, aVal) {
return this.wrapPromise(lazy.AsyncPrefs.set(aPref, aVal));
}
RPMGetFormatURLPref(aFormatURL) {
return Services.urlFormatter.formatURLPref(aFormatURL);
}
RPMIsWindowPrivate() {
return lazy.PrivateBrowsingUtils.isContentWindowPrivate(this.contentWindow);
}
}