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/. */
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { Log } from "resource://gre/modules/Log.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
if (AppConstants.platform == "android") {
ChromeUtils.defineESModuleGetters(lazy, {
AndroidAppender: "resource://gre/modules/AndroidLog.sys.mjs",
});
}
export var GeckoViewUtils = {
/**
* Define a lazy getter that loads an object from external code, and
* optionally handles observer and/or message manager notifications for the
* object, so the object only loads when a notification is received.
*
* @param scope Scope for holding the loaded object.
* @param name Name of the object to load.
* @param service If specified, load the object from a JS component; the
* component must include the line
* "this.wrappedJSObject = this;" in its constructor.
* @param module If specified, load the object from a JS module.
* @param init Optional post-load initialization function.
* @param observers If specified, listen to specified observer notifications.
* @param ppmm If specified, listen to specified process messages.
* @param mm If specified, listen to specified frame messages.
* @param ged If specified, listen to specified global EventDispatcher events.
* @param once if true, only listen to the specified
* events/messages/notifications once.
*/
addLazyGetter(
scope,
name,
{ service, module, handler, observers, ppmm, mm, ged, init, once }
) {
ChromeUtils.defineLazyGetter(scope, name, _ => {
let ret = undefined;
if (module) {
ret = ChromeUtils.importESModule(module)[name];
} else if (service) {
ret = Cc[service].getService(Ci.nsISupports).wrappedJSObject;
} else if (typeof handler === "function") {
ret = {
handleEvent: handler,
observe: handler,
onEvent: handler,
receiveMessage: handler,
};
} else if (handler) {
ret = handler;
}
if (ret && init) {
init.call(scope, ret);
}
return ret;
});
if (observers) {
const observer = (subject, topic, data) => {
Services.obs.removeObserver(observer, topic);
if (!once) {
Services.obs.addObserver(scope[name], topic);
}
scope[name].observe(subject, topic, data); // Explicitly notify new observer
};
observers.forEach(topic => Services.obs.addObserver(observer, topic));
}
if (!this.IS_PARENT_PROCESS) {
// ppmm, mm, and ged are only available in the parent process.
return;
}
const addMMListener = (target, names) => {
const listener = msg => {
target.removeMessageListener(msg.name, listener);
if (!once) {
target.addMessageListener(msg.name, scope[name]);
}
scope[name].receiveMessage(msg);
};
names.forEach(msg => target.addMessageListener(msg, listener));
};
if (ppmm) {
addMMListener(Services.ppmm, ppmm);
}
if (mm) {
addMMListener(Services.mm, mm);
}
if (ged) {
const listener = (event, data, callback) => {
lazy.EventDispatcher.instance.unregisterListener(listener, event);
if (!once) {
lazy.EventDispatcher.instance.registerListener(scope[name], event);
}
scope[name].onEvent(event, data, callback);
};
lazy.EventDispatcher.instance.registerListener(listener, ged);
}
},
_addLazyListeners(events, handler, scope, name, addFn, handleFn) {
if (!handler) {
handler = _ =>
Array.isArray(name) ? name.map(n => scope[n]) : scope[name];
}
const listener = (...args) => {
let handlers = handler(...args);
if (!handlers) {
return;
}
if (!Array.isArray(handlers)) {
handlers = [handlers];
}
handleFn(handlers, listener, args);
};
if (Array.isArray(events)) {
addFn(events, listener);
} else {
addFn([events], listener);
}
},
/**
* Add lazy event listeners that only load the actual handler when an event
* is being handled.
*
* @param target Event target for the event listeners.
* @param events Event name as a string or array.
* @param handler If specified, function that, for a given event, returns the
* actual event handler as an object or an array of objects.
* If handler is not specified, the actual event handler is
* specified using the scope and name pair.
* @param scope See handler.
* @param name See handler.
* @param options Options for addEventListener.
*/
addLazyEventListener(target, events, { handler, scope, name, options }) {
this._addLazyListeners(
events,
handler,
scope,
name,
(events, listener) => {
events.forEach(event =>
target.addEventListener(event, listener, options)
);
},
(handlers, listener, args) => {
if (!options || !options.once) {
target.removeEventListener(args[0].type, listener, options);
handlers.forEach(handler =>
target.addEventListener(args[0].type, handler, options)
);
}
handlers.forEach(handler => handler.handleEvent(args[0]));
}
);
},
/**
* Add lazy pref observers, and only load the actual handler once the pref
* value changes from default, and every time the pref value changes
* afterwards.
*
* @param aPrefs Prefs as an object or array. Each pref object has fields
* "name" and "default", indicating the name and default value
* of the pref, respectively.
* @param handler If specified, function that, for a given pref, returns the
* actual event handler as an object or an array of objects.
* If handler is not specified, the actual event handler is
* specified using the scope and name pair.
* @param scope See handler.
* @param name See handler.
* @param once If true, only observe the specified prefs once.
*/
addLazyPrefObserver(aPrefs, { handler, scope, name, once }) {
this._addLazyListeners(
aPrefs,
handler,
scope,
name,
(prefs, observer) => {
prefs.forEach(pref => Services.prefs.addObserver(pref.name, observer));
prefs.forEach(pref => {
if (pref.default === undefined) {
return;
}
let value;
switch (typeof pref.default) {
case "string":
value = Services.prefs.getCharPref(pref.name, pref.default);
break;
case "number":
value = Services.prefs.getIntPref(pref.name, pref.default);
break;
case "boolean":
value = Services.prefs.getBoolPref(pref.name, pref.default);
break;
}
if (pref.default !== value) {
// Notify observer if value already changed from default.
observer(Services.prefs, "nsPref:changed", pref.name);
}
});
},
(handlers, observer, args) => {
if (!once) {
Services.prefs.removeObserver(args[2], observer);
handlers.forEach(() => Services.prefs.addObserver(args[2], observer));
}
handlers.forEach(handler => handler.observe(...args));
}
);
},
getRootDocShell(aWin) {
if (!aWin) {
return null;
}
let docShell;
try {
docShell = aWin.QueryInterface(Ci.nsIDocShell);
} catch (e) {
docShell = aWin.docShell;
}
return docShell.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor);
},
/**
* Return the outermost chrome DOM window (the XUL window) for a given DOM
* window, in the parent process.
*
* @param aWin a DOM window.
*/
getChromeWindow(aWin) {
const docShell = this.getRootDocShell(aWin);
return docShell && docShell.domWindow;
},
/**
* Return the content frame message manager (aka the frame script global
* object) for a given DOM window, in a child process.
*
* @param aWin a DOM window.
*/
getContentFrameMessageManager(aWin) {
const docShell = this.getRootDocShell(aWin);
return docShell && docShell.getInterface(Ci.nsIBrowserChild).messageManager;
},
/**
* Return the per-nsWindow EventDispatcher for a given DOM window, in either
* the parent process or a child process.
*
* @param aWin a DOM window.
*/
getDispatcherForWindow(aWin) {
try {
if (!this.IS_PARENT_PROCESS) {
const mm = this.getContentFrameMessageManager(aWin.top || aWin);
return mm && lazy.EventDispatcher.forMessageManager(mm);
}
const win = this.getChromeWindow(aWin.top || aWin);
if (!win.closed) {
return win.WindowEventDispatcher || lazy.EventDispatcher.for(win);
}
} catch (e) {}
return null;
},
/**
* Return promise for waiting for finishing PanZoomState.
*
* @param aWindow a DOM window.
* @return promise
*/
waitForPanZoomState(aWindow) {
return new Promise((resolve, reject) => {
if (
!aWindow?.windowUtils.asyncPanZoomEnabled ||
!Services.prefs.getBoolPref("apz.zoom-to-focused-input.enabled")
) {
// No zoomToFocusedInput.
resolve();
return;
}
let timerId = 0;
const panZoomState = (aSubject, aTopic, aData) => {
if (timerId != 0) {
// aWindow may be dead object now.
try {
lazy.clearTimeout(timerId);
} catch (e) {}
timerId = 0;
}
if (aData === "NOTHING") {
Services.obs.removeObserver(panZoomState, "PanZoom:StateChange");
resolve();
}
};
Services.obs.addObserver(panZoomState, "PanZoom:StateChange");
// "GeckoView:ZoomToInput" has the timeout as 500ms when window isn't
// resized (it means on-screen-keyboard is already shown).
// So after up to 500ms, APZ event is sent. So we need to wait for more
// 500ms.
timerId = lazy.setTimeout(() => {
// PanZoom state isn't changed. zoomToFocusedInput will return error.
Services.obs.removeObserver(panZoomState, "PanZoom:StateChange");
reject();
}, 600);
});
},
/**
* Add logging functions to the specified scope that forward to the given
* Log.sys.mjs logger. Currently "debug" and "warn" functions are supported. To
* log something, call the function through a template literal:
*
* function foo(bar, baz) {
* debug `hello world`;
* debug `foo called with ${bar} as bar`;
* warn `this is a warning for ${baz}`;
* }
*
* An inline format can also be used for logging:
*
* let bar = 42;
* do_something(bar); // No log.
* do_something(debug.foo = bar); // Output "foo = 42" to the log.
*
* @param aTag Name of the Log.sys.mjs logger to forward logs to.
* @param aScope Scope to add the logging functions to.
*/
initLogging(aTag, aScope) {
aScope = aScope || {};
const tag = "GeckoView." + aTag.replace(/^GeckoView\.?/, "");
// Only provide two levels for simplicity.
// For "info", use "debug" instead.
// For "error", throw an actual JS error instead.
for (const level of ["DEBUG", "WARN"]) {
const log = (strings, ...exprs) =>
this._log(log.logger, level, strings, exprs);
ChromeUtils.defineLazyGetter(log, "logger", _ => {
const logger = Log.repository.getLogger(tag);
logger.parent = this.rootLogger;
return logger;
});
aScope[level.toLowerCase()] = new Proxy(log, {
set: (obj, prop, value) => obj([prop + " = ", ""], value) || true,
});
}
return aScope;
},
get rootLogger() {
if (!this._rootLogger) {
this._rootLogger = Log.repository.getLogger("GeckoView");
// On Android, we'll log to the native android logcat output using
// __android_log_write. On iOS, fall back to a dump appender.
if (AppConstants.platform == "android") {
this._rootLogger.addAppender(new lazy.AndroidAppender());
} else {
this._rootLogger.addAppender(new Log.DumpAppender());
}
this._rootLogger.manageLevelFromPref("geckoview.logging");
}
return this._rootLogger;
},
_log(aLogger, aLevel, aStrings, aExprs) {
if (!Array.isArray(aStrings)) {
const [, file, line] = new Error().stack.match(/.*\n.*\n.*@(.*):(\d+):/);
throw Error(
`Expecting template literal: ${aLevel} \`foo \${bar}\``,
file,
+line
);
}
if (aLogger.level > Log.Level.Numbers[aLevel]) {
// Log disabled.
return;
}
// Do some GeckoView-specific formatting:
// * Remove newlines so long log lines can be put into multiple lines:
// debug `foo=${foo}
// bar=${bar}`;
const strs = Array.from(aStrings);
const regex = /\n\s*/g;
for (let i = 0; i < strs.length; i++) {
strs[i] = strs[i].replace(regex, " ");
}
// * Heuristically format flags as hex.
// * Heuristically format nsresult as string name or hex.
for (let i = 0; i < aExprs.length; i++) {
const expr = aExprs[i];
switch (typeof expr) {
case "number":
if (expr > 0 && /\ba?[fF]lags?[\s=:]+$/.test(strs[i])) {
// Likely a flag; display in hex.
aExprs[i] = `0x${expr.toString(0x10)}`;
} else if (expr >= 0 && /\b(a?[sS]tatus|rv)[\s=:]+$/.test(strs[i])) {
// Likely an nsresult; display in name or hex.
aExprs[i] = `0x${expr.toString(0x10)}`;
for (const name in Cr) {
if (expr === Cr[name]) {
aExprs[i] = name;
break;
}
}
}
break;
}
}
aLogger[aLevel.toLowerCase()](strs, ...aExprs);
},
/**
* Checks whether the principal is supported for permissions.
*
* @param {nsIPrincipal} principal
* The principal to check.
*
* @return {boolean} if the principal is supported.
*/
isSupportedPermissionsPrincipal(principal) {
if (!principal) {
return false;
}
if (!(principal instanceof Ci.nsIPrincipal)) {
throw new Error(
"Argument passed as principal is not an instance of Ci.nsIPrincipal"
);
}
return this.isSupportedPermissionsScheme(principal.scheme);
},
/**
* Checks whether we support managing permissions for a specific scheme.
* @param {string} scheme - Scheme to test.
* @returns {boolean} Whether the scheme is supported.
*/
isSupportedPermissionsScheme(scheme) {
return ["http", "https", "moz-extension", "file"].includes(scheme);
},
/**
* Attach nsIOpenWindowInfo when opening GeckoSession
*
* @param {string} aSessionId A session id
* @param {nsIOpenWindowInfo} aOpenWindowInfo Attached nsIOpendWindowInfo
* @param {string} aName A window name
* @returns {Promise} resolved when nsIOpenWindowInfo is attached
*/
waitAndSetupWindow(aSessionId, aOpenWindowInfo, aName) {
if (!aSessionId) {
return Promise.reject();
}
return new Promise((resolve, reject) => {
const handler = {
observe(aSubject, aTopic) {
if (
aTopic === "geckoview-window-created" &&
aSubject.name === aSessionId
) {
// This value will be read by nsFrameLoader while it is being initialized.
aSubject.browser.openWindowInfo = aOpenWindowInfo;
// Gecko will use this attribute to set the name of the opened window.
if (aName) {
aSubject.browser.setAttribute("name", aName);
}
if (
!aOpenWindowInfo.isRemote &&
aSubject.browser.hasAttribute("remote")
) {
// We cannot start in remote mode when we have an opener.
aSubject.browser.setAttribute("remote", "false");
aSubject.browser.removeAttribute("remoteType");
}
Services.obs.removeObserver(handler, "geckoview-window-created");
if (!aSubject) {
reject();
return;
}
resolve(aSubject);
}
},
};
// This event is emitted from createBrowser() in geckoview.js
Services.obs.addObserver(handler, "geckoview-window-created");
});
},
};
ChromeUtils.defineLazyGetter(
GeckoViewUtils,
"IS_PARENT_PROCESS",
_ => Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT
);