/* 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";
const {
FrontClassWithSpec,
types,
registerFront,
} = require(
"resource://devtools/shared/protocol.js");
const { walkerSpec } = require(
"resource://devtools/shared/specs/walker.js");
const {
safeAsyncMethod,
} = require(
"resource://devtools/shared/async-utils.js");
/**
* Client side of the DOM walker.
*/
class WalkerFront
extends FrontClassWithSpec(walkerSpec) {
constructor(client, targetFront, parentFront) {
super(client, targetFront, parentFront);
this._isPicking =
false;
this._orphaned =
new Set();
this._retainedOrphans =
new Set();
// Set to true if cleanup should be requested after every mutation list.
this.autoCleanup =
true;
this._rootNodePromise =
new Promise(
r => (
this._rootNodePromiseResolve = r)
);
this._onRootNodeAvailable =
this._onRootNodeAvailable.bind(
this);
this._onRootNodeDestroyed =
this._onRootNodeDestroyed.bind(
this);
// pick/cancelPick requests can be triggered while the Walker is being destroyed.
this.pick = safeAsyncMethod(
this.pick.bind(
this), () =>
this.isDestroyed());
this.cancelPick = safeAsyncMethod(
this.cancelPick.bind(
this), () =>
this.isDestroyed()
);
this.before(
"new-mutations",
this.onMutations.bind(
this));
// Those events will be emitted if watchRootNode was called on the
// corresponding WalkerActor, which should be handled by the ResourceCommand
// as long as a consumer is watching for root-node resources.
// This should be fixed by using watchResources directly from the walker
// front, either with the ROOT_NODE resource, or with the DOCUMENT_EVENT
// resource. See Bug 1663973.
this.on(
"root-available",
this._onRootNodeAvailable);
this.on(
"root-destroyed",
this._onRootNodeDestroyed);
}
// Update the object given a form representation off the wire.
form(json) {
this.actorID = json.actor;
// The rootNode property should usually be provided via watchRootNode.
// However tests are currently using the walker front without explicitly
// calling watchRootNode, so we keep this assignment as a fallback.
this.rootNode = types.getType(
"domnode").read(json.root,
this);
// Bug 1861328: boolean set to true when color scheme can't be changed (happens when `privacy.resistFingerprinting` is set to true)
this.rfpCSSColorScheme = json.rfpCSSColorScheme;
this.traits = json.traits;
}
/**
* Clients can use walker.rootNode to get the current root node of the
* walker, but during a reload the root node might be null. This
* method returns a promise that will resolve to the root node when it is
* set.
*/
async getRootNode() {
let rootNode =
this.rootNode;
if (!rootNode) {
rootNode = await
this._rootNodePromise;
}
return rootNode;
}
/**
* When reading an actor form off the wire, we want to hook it up to its
* parent or host front. The protocol guarantees that the parent will
* be seen by the client in either a previous or the current request.
* So if we've already seen this parent return it, otherwise create
* a bare-bones stand-in node. The stand-in node will be updated
* with a real form by the end of the deserialization.
*/
ensureDOMNodeFront(id) {
const front =
this.getActorByID(id);
if (front) {
return front;
}
return types.getType(
"domnode").read({ actor: id },
this,
"standin");
}
/**
* See the documentation for WalkerActor.prototype.retainNode for
* information on retained nodes.
*
* From the client's perspective, `retainNode` can fail if the node in
* question is removed from the ownership tree before the `retainNode`
* request reaches the server. This can only happen if the client has
* asked the server to release nodes but hasn't gotten a response
* yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
* set is outstanding.
*
* If either of those requests is outstanding AND releases the retained
* node, this request will fail with noSuchActor, but the ownership tree
* will stay in a consistent state.
*
* Because the protocol guarantees that requests will be processed and
* responses received in the order they were sent, we get the right
* semantics by setting our local retained flag on the node only AFTER
* a SUCCESSFUL retainNode call.
*/
async retainNode(node) {
await
super.retainNode(node);
node.retained =
true;
}
async unretainNode(node) {
await
super.unretainNode(node);
node.retained =
false;
if (
this._retainedOrphans.has(node)) {
this._retainedOrphans.
delete(node);
this._releaseFront(node);
}
}
releaseNode(node, options = {}) {
// NodeFront.destroy will destroy children in the ownership tree too,
// mimicking what the server will do here.
const actorID = node.actorID;
this._releaseFront(node, !!options.force);
return super.releaseNode({ actorID });
}
async findInspectingNode() {
const response = await
super.findInspectingNode();
return response.node;
}
async querySelector(queryNode, selector) {
const response = await
super.querySelector(queryNode, selector);
return response.node;
}
async getIdrefNode(queryNode, id) {
const response = await
super.getIdrefNode(queryNode, id);
return response.node;
}
async getNodeActorFromWindowID(windowID) {
const response = await
super.getNodeActorFromWindowID(windowID);
return response ? response.node :
null;
}
async getNodeActorFromContentDomReference(contentDomReference) {
const response = await
super.getNodeActorFromContentDomReference(
contentDomReference
);
return response ? response.node :
null;
}
async getStyleSheetOwnerNode(styleSheetActorID) {
const response = await
super.getStyleSheetOwnerNode(styleSheetActorID);
return response ? response.node :
null;
}
async getNodeFromActor(actorID, path) {
const response = await
super.getNodeFromActor(actorID, path);
return response ? response.node :
null;
}
_releaseFront(node, force) {
if (node.retained && !force) {
node.reparent(
null);
this._retainedOrphans.add(node);
return;
}
if (node.retained) {
// Forcing a removal.
this._retainedOrphans.
delete(node);
}
// Release any children
for (
const child of node.treeChildren()) {
this._releaseFront(child, force);
}
// All children will have been removed from the node by this point.
node.reparent(
null);
node.destroy();
}
/**
* Get any unprocessed mutation records and process them.
*/
// eslint-disable-next-line complexity
async getMutations(options = {}) {
const mutations = await
super.getMutations(options);
const emitMutations = [];
for (
const change of mutations) {
// The target is only an actorID, get the associated front.
const targetID = change.target;
const targetFront =
this.getActorByID(targetID);
if (!targetFront) {
console.warn(
"Got a mutation for an unexpected actor: " +
targetID +
", please file a bug on bugzilla.mozilla.org!"
);
console.trace();
continue;
}
const emittedMutation = Object.assign(change, { target: targetFront });
if (change.type ===
"childList") {
// Update the ownership tree according to the mutation record.
const addedFronts = [];
const removedFronts = [];
for (
const removed of change.removed) {
const removedFront =
this.getActorByID(removed);
if (!removedFront) {
console.error(
"Got a removal of an actor we didn't know about: " + removed
);
continue;
}
// Remove from the ownership tree
removedFront.reparent(
null);
// This node is orphaned unless we get it in the 'added' list
// eventually.
this._orphaned.add(removedFront);
removedFronts.push(removedFront);
}
for (
const added of change.added) {
const addedFront =
this.getActorByID(added);
if (!addedFront) {
console.error(
"Got an addition of an actor we didn't know " +
"about: " + added
);
continue;
}
addedFront.reparent(targetFront);
// The actor is reconnected to the ownership tree, unorphan
// it.
this._orphaned.
delete(addedFront);
addedFronts.push(addedFront);
}
// Before passing to users, replace the added and removed actor
// ids with front in the mutation record.
emittedMutation.added = addedFronts;
emittedMutation.removed = removedFronts;
// If this is coming from a DOM mutation, the actor's numChildren
// was passed in. Otherwise, it is simulated from a frame load or
// unload, so don't change the front's form.
if (
"numChildren" in change) {
targetFront._form.numChildren = change.numChildren;
}
}
else if (change.type ===
"shadowRootAttached") {
targetFront._form.isShadowHost =
true;
}
else if (change.type ===
"customElementDefined") {
targetFront._form.customElementLocation = change.customElementLocation;
}
else if (change.type ===
"unretained") {
// Retained orphans were force-released without the intervention of
// client (probably a navigated frame).
for (
const released of change.nodes) {
const releasedFront =
this.getActorByID(released);
this._retainedOrphans.
delete(released);
this._releaseFront(releasedFront,
true);
}
}
else {
targetFront.updateMutation(change);
}
// Update the inlineTextChild property of the target for a selected list of
// mutation types.
if (
change.type ===
"inlineTextChild" ||
change.type ===
"childList" ||
change.type ===
"shadowRootAttached"
) {
if (change.inlineTextChild) {
targetFront.inlineTextChild = types
.getType(
"domnode")
.read(change.inlineTextChild,
this);
}
else {
targetFront.inlineTextChild = undefined;
}
}
emitMutations.push(emittedMutation);
}
if (options.cleanup) {
for (
const node of
this._orphaned) {
// This will move retained nodes to this._retainedOrphans.
this._releaseFront(node);
}
this._orphaned =
new Set();
}
this.emit(
"mutations", emitMutations);
}
/**
* Handle the `new-mutations` notification by fetching the
* available mutation records.
*/
onMutations() {
// Fetch and process the mutations.
this.getMutations({ cleanup:
this.autoCleanup }).
catch(() => {});
}
isLocal() {
return !!
this.conn._transport._serverConnection;
}
async removeNode(node) {
const previousSibling = await
this.previousSibling(node);
const nextSibling = await
super.removeNode(node);
return {
previousSibling,
nextSibling,
};
}
async children(node, options) {
if (!node.useChildTargetToFetchChildren) {
return super.children(node, options);
}
const target = await node.connectToFrame();
// We had several issues in the past where `connectToFrame` was returning the same
// target as the owner document one, which led to the inspector being broken.
// Ultimately, we shouldn't get to this point (fix should happen in connectToFrame or
// on the server, e.g. for Bug 1752342), but at least this will serve as a safe guard
// so we don't freeze/crash the inspector.
if (
target ==
this.targetFront &&
Services.prefs.getBoolPref(
"devtools.testing.bypass-walker-children-iframe-guard",
false
) !==
true
) {
console.warn(
"connectToFrame returned an unexpected target");
return {
nodes: [],
hasFirst:
true,
hasLast:
true,
};
}
const walker = (await target.getFront(
"inspector")).walker;
// Finally retrieve the NodeFront of the remote frame's document
const documentNode = await walker.getRootNode();
// Force reparenting through the remote frame boundary.
documentNode.reparent(node);
// And return the same kind of response `walker.children` returns
return {
nodes: [documentNode],
hasFirst:
true,
hasLast:
true,
};
}
/**
* Ensure that the RootNode of this Walker has the right parent NodeFront.
*
* This method does nothing if we are on the top level target's WalkerFront,
* as the RootNode won't have any parent.
*
* Otherwise, if we are in an iframe's WalkerFront, we would expect the parent
* of the RootNode (i.e. the NodeFront for the document loaded within the iframe)
* to be the <iframe>'s NodeFront. Because of fission, the two NodeFront may refer
* to DOM Element running in distinct processes and so the NodeFront comes from
* two distinct Targets and two distinct WalkerFront.
* This is why we need this manual "reparent" code to do the glue between the
* two documents.
*/
async reparentRemoteFrame() {
const parentTarget = await
this.targetFront.getParentTarget();
if (!parentTarget) {
return;
}
// Don't reparent if we are on the top target
if (parentTarget ==
this.targetFront) {
return;
}
// Get the NodeFront for the embedder element
// i.e. the <iframe> element which is hosting the document that
const parentWalker = (await parentTarget.getFront(
"inspector")).walker;
// As this <iframe> most likely runs in another process, we have to get it through the parent
// target's WalkerFront.
const parentNode = (
await parentWalker.getEmbedderElement(
this.targetFront.browsingContextID)
).node;
// Finally, set this embedder element's node front as the
const documentNode = await
this.getRootNode();
documentNode.reparent(parentNode);
}
_onRootNodeAvailable(rootNode) {
if (rootNode.isTopLevelDocument) {
this.rootNode = rootNode;
this._rootNodePromiseResolve(
this.rootNode);
}
}
_onRootNodeDestroyed(rootNode) {
if (rootNode.isTopLevelDocument) {
this._rootNodePromise =
new Promise(
r => (
this._rootNodePromiseResolve = r)
);
this.rootNode =
null;
}
}
/**
* Start the element picker on the debuggee target.
* @param {Boolean} doFocus - Optionally focus the content area once the picker is
* activated.
*/
pick(doFocus) {
if (
this._isPicking) {
return Promise.resolve();
}
this._isPicking =
true;
return super.pick(
doFocus,
this.targetFront.commands.descriptorFront.isLocalTab
);
}
/**
* Stop the element picker.
*/
cancelPick() {
if (!
this._isPicking) {
return Promise.resolve();
}
this._isPicking =
false;
return super.cancelPick();
}
}
exports.WalkerFront = WalkerFront;
registerFront(WalkerFront);