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 { ContentProcessDomain } from "chrome://remote/content/cdp/domains/ContentProcessDomain.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
});
const { LOAD_FLAGS_BYPASS_CACHE, LOAD_FLAGS_BYPASS_PROXY, LOAD_FLAGS_NONE } =
Ci.nsIWebNavigation;
export class Page extends ContentProcessDomain {
constructor(session) {
super(session);
this.enabled = false;
this.lifecycleEnabled = false;
// script id => { source, worldName }
this.scriptsToEvaluateOnLoad = new Map();
this.worldsToEvaluateOnLoad = new Set();
// This map is used to keep a reference to the loader id for
// those Page events, which do not directly rely on
// Network events. This might be a temporary solution until
// the Network observer could be queried for that. But right
// now this lives in the parent process.
this.frameIdToLoaderId = new Map();
this._onFrameAttached = this._onFrameAttached.bind(this);
this._onFrameDetached = this._onFrameDetached.bind(this);
this._onFrameNavigated = this._onFrameNavigated.bind(this);
this._onScriptLoaded = this._onScriptLoaded.bind(this);
this.session.contextObserver.on("script-loaded", this._onScriptLoaded);
}
destructor() {
this.setLifecycleEventsEnabled({ enabled: false });
this.session.contextObserver.off("script-loaded", this._onScriptLoaded);
this.disable();
super.destructor();
}
// commands
async enable() {
if (!this.enabled) {
this.session.contextObserver.on("frame-attached", this._onFrameAttached);
this.session.contextObserver.on("frame-detached", this._onFrameDetached);
this.session.contextObserver.on(
"frame-navigated",
this._onFrameNavigated
);
this.chromeEventHandler.addEventListener("readystatechange", this, {
mozSystemGroup: true,
capture: true,
});
this.chromeEventHandler.addEventListener("pagehide", this, {
mozSystemGroup: true,
});
this.chromeEventHandler.addEventListener("unload", this, {
mozSystemGroup: true,
capture: true,
});
this.chromeEventHandler.addEventListener("DOMContentLoaded", this, {
mozSystemGroup: true,
});
this.chromeEventHandler.addEventListener("hashchange", this, {
mozSystemGroup: true,
capture: true,
});
this.chromeEventHandler.addEventListener("load", this, {
mozSystemGroup: true,
capture: true,
});
this.chromeEventHandler.addEventListener("pageshow", this, {
mozSystemGroup: true,
});
this.enabled = true;
}
}
disable() {
if (this.enabled) {
this.session.contextObserver.off("frame-attached", this._onFrameAttached);
this.session.contextObserver.off("frame-detached", this._onFrameDetached);
this.session.contextObserver.off(
"frame-navigated",
this._onFrameNavigated
);
this.chromeEventHandler.removeEventListener("readystatechange", this, {
mozSystemGroup: true,
capture: true,
});
this.chromeEventHandler.removeEventListener("pagehide", this, {
mozSystemGroup: true,
});
this.chromeEventHandler.removeEventListener("unload", this, {
mozSystemGroup: true,
capture: true,
});
this.chromeEventHandler.removeEventListener("DOMContentLoaded", this, {
mozSystemGroup: true,
});
this.chromeEventHandler.removeEventListener("hashchange", this, {
mozSystemGroup: true,
capture: true,
});
this.chromeEventHandler.removeEventListener("load", this, {
mozSystemGroup: true,
capture: true,
});
this.chromeEventHandler.removeEventListener("pageshow", this, {
mozSystemGroup: true,
});
this.enabled = false;
}
}
async reload(options = {}) {
const { ignoreCache } = options;
let flags = LOAD_FLAGS_NONE;
if (ignoreCache) {
flags |= LOAD_FLAGS_BYPASS_CACHE;
flags |= LOAD_FLAGS_BYPASS_PROXY;
}
this.docShell.reload(flags);
}
getFrameTree() {
const getFrames = context => {
const frameTree = {
frame: this._getFrameDetails({ context }),
};
if (context.children.length) {
const frames = [];
for (const childContext of context.children) {
frames.push(getFrames(childContext));
}
frameTree.childFrames = frames;
}
return frameTree;
};
return {
frameTree: getFrames(this.docShell.browsingContext),
};
}
/**
* Enqueues given script to be evaluated in every frame upon creation
*
* If `worldName` is specified, creates an execution context with the given name
* and evaluates given script in it.
*
* At this time, queued scripts do not get evaluated, hence `source` is marked as
* "unsupported".
*
* @param {object} options
* @param {string} options.source (not supported)
* @param {string=} options.worldName
* @returns {string} Page.ScriptIdentifier
*/
addScriptToEvaluateOnNewDocument(options = {}) {
const { source, worldName } = options;
if (worldName) {
this.worldsToEvaluateOnLoad.add(worldName);
}
const identifier = lazy.generateUUID();
this.scriptsToEvaluateOnLoad.set(identifier, { worldName, source });
return { identifier };
}
/**
* Creates an isolated world for the given frame.
*
* Really it just creates an execution context with label "isolated".
*
* @param {object} options
* @param {string} options.frameId
* Id of the frame in which the isolated world should be created.
* @param {string=} options.worldName
* An optional name which is reported in the Execution Context.
* @param {boolean=} options.grantUniversalAccess (not supported)
* This is a powerful option, use with caution.
*
* @returns {number} Runtime.ExecutionContextId
* Execution context of the isolated world.
*/
createIsolatedWorld(options = {}) {
const { frameId, worldName } = options;
if (typeof frameId != "string") {
throw new TypeError("frameId: string value expected");
}
if (!["undefined", "string"].includes(typeof worldName)) {
throw new TypeError("worldName: string value expected");
}
const Runtime = this.session.domains.get("Runtime");
const contexts = Runtime._getContextsForFrame(frameId);
if (!contexts.length) {
throw new Error("No frame for given id found");
}
const defaultContext = Runtime._getDefaultContextForWindow(
contexts[0].windowId
);
const window = defaultContext.window;
const executionContextId = Runtime._onContextCreated("context-created", {
windowId: window.windowGlobalChild.innerWindowId,
window,
isDefault: false,
contextName: worldName,
contextType: "isolated",
});
return { executionContextId };
}
/**
* Controls whether page will emit lifecycle events.
*
* @param {object} options
* @param {boolean} options.enabled
* If true, starts emitting lifecycle events.
*/
setLifecycleEventsEnabled(options = {}) {
const { enabled } = options;
this.lifecycleEnabled = enabled;
}
url() {
return this.content.location.href;
}
_onFrameAttached(name, { frameId, window }) {
const bc = BrowsingContext.get(frameId);
// Don't emit for top-level browsing contexts
if (!bc.parent) {
return;
}
// TODO: Use a unique identifier for frames (bug 1605359)
this.emit("Page.frameAttached", {
frameId: frameId.toString(),
parentFrameId: bc.parent.id.toString(),
stack: null,
});
// Usually both events are emitted when the "pagehide" event is received.
// But this wont happen for a new window or frame when the initial
// about:blank page has already loaded, and is being replaced with the
// final document.
if (!window.document.isInitialDocument) {
this.emit("Page.frameStartedLoading", { frameId: frameId.toString() });
const loaderId = this.frameIdToLoaderId.get(frameId);
const timestamp = Date.now() / 1000;
this.emitLifecycleEvent(frameId, loaderId, "init", timestamp);
}
}
_onFrameDetached(name, { frameId }) {
const bc = BrowsingContext.get(frameId);
// Don't emit for top-level browsing contexts
if (!bc.parent) {
return;
}
// TODO: Use a unique identifier for frames (bug 1605359)
this.emit("Page.frameDetached", { frameId: frameId.toString() });
}
_onFrameNavigated(name, { frameId }) {
const bc = BrowsingContext.get(frameId);
this.emit("Page.frameNavigated", {
frame: this._getFrameDetails({ context: bc }),
});
}
/**
* @param {string} name
* The event name.
* @param {object=} options
* @param {number} options.windowId
* The inner window id of the window the script has been loaded for.
* @param {Window} options.window
* The window object of the document.
*/
_onScriptLoaded(name, options = {}) {
const { windowId, window } = options;
const Runtime = this.session.domains.get("Runtime");
for (const world of this.worldsToEvaluateOnLoad) {
Runtime._onContextCreated("context-created", {
windowId,
window,
isDefault: false,
contextName: world,
contextType: "isolated",
});
}
// TODO evaluate each onNewDoc script in the appropriate world
}
emitLifecycleEvent(frameId, loaderId, name, timestamp) {
if (this.lifecycleEnabled) {
this.emit("Page.lifecycleEvent", {
frameId: frameId.toString(),
loaderId,
name,
timestamp,
});
}
}
handleEvent({ type, target }) {
const timestamp = Date.now() / 1000;
// Some events such as "hashchange" use the window as the target, while
// others have a document.
const win = Window.isInstance(target) ? target : target.defaultView;
const frameId = win.docShell.browsingContext.id;
const isFrame = !!win.docShell.browsingContext.parent;
const loaderId = this.frameIdToLoaderId.get(frameId);
const url = win.location.href;
switch (type) {
case "DOMContentLoaded":
if (!isFrame) {
this.emit("Page.domContentEventFired", { timestamp });
}
this.emitLifecycleEvent(
frameId,
loaderId,
"DOMContentLoaded",
timestamp
);
break;
case "hashchange":
this.emit("Page.navigatedWithinDocument", {
frameId: frameId.toString(),
url,
});
break;
case "pagehide":
// Maybe better to bound to "unload" once we can register for this event
this.emit("Page.frameStartedLoading", { frameId: frameId.toString() });
this.emitLifecycleEvent(frameId, loaderId, "init", timestamp);
break;
case "load":
if (!isFrame) {
this.emit("Page.loadEventFired", { timestamp });
}
this.emitLifecycleEvent(frameId, loaderId, "load", timestamp);
// XXX this should most likely be sent differently
this.emit("Page.frameStoppedLoading", { frameId: frameId.toString() });
break;
case "readystatechange":
if (this.content.document.readyState === "loading") {
this.emitLifecycleEvent(frameId, loaderId, "init", timestamp);
}
}
}
_updateLoaderId(data) {
const { frameId, loaderId } = data;
this.frameIdToLoaderId.set(frameId, loaderId);
}
_contentRect() {
const docEl = this.content.document.documentElement;
return {
x: 0,
y: 0,
width: docEl.scrollWidth,
height: docEl.scrollHeight,
};
}
_devicePixelRatio() {
return (
this.content.browsingContext.overrideDPPX || this.content.devicePixelRatio
);
}
_getFrameDetails({ context, id }) {
const bc = context || BrowsingContext.get(id);
const frame = bc.embedderElement;
return {
id: bc.id.toString(),
parentId: bc.parent?.id.toString(),
loaderId: this.frameIdToLoaderId.get(bc.id),
url: bc.docShell.domWindow.location.href,
name: frame?.id || frame?.name,
securityOrigin: null,
mimeType: null,
};
}
_getScrollbarSize() {
const scrollbarHeight = {};
const scrollbarWidth = {};
this.content.windowUtils.getScrollbarSize(
false,
scrollbarWidth,
scrollbarHeight
);
return {
width: scrollbarWidth.value,
height: scrollbarHeight.value,
};
}
_layoutViewport() {
const scrollbarSize = this._getScrollbarSize();
return {
pageX: this.content.pageXOffset,
pageY: this.content.pageYOffset,
clientWidth: this.content.innerWidth - scrollbarSize.width,
clientHeight: this.content.innerHeight - scrollbarSize.height,
};
}
}