summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/inspector/inspector.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/server/actors/inspector/inspector.js
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/actors/inspector/inspector.js')
-rw-r--r--devtools/server/actors/inspector/inspector.js355
1 files changed, 355 insertions, 0 deletions
diff --git a/devtools/server/actors/inspector/inspector.js b/devtools/server/actors/inspector/inspector.js
new file mode 100644
index 0000000000..cdfa892889
--- /dev/null
+++ b/devtools/server/actors/inspector/inspector.js
@@ -0,0 +1,355 @@
+/* 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";
+
+/**
+ * Here's the server side of the remote inspector.
+ *
+ * The WalkerActor is the client's view of the debuggee's DOM. It's gives
+ * the client a tree of NodeActor objects.
+ *
+ * The walker presents the DOM tree mostly unmodified from the source DOM
+ * tree, but with a few key differences:
+ *
+ * - Empty text nodes are ignored. This is pretty typical of developer
+ * tools, but maybe we should reconsider that on the server side.
+ * - iframes with documents loaded have the loaded document as the child,
+ * the walker provides one big tree for the whole document tree.
+ *
+ * There are a few ways to get references to NodeActors:
+ *
+ * - When you first get a WalkerActor reference, it comes with a free
+ * reference to the root document's node.
+ * - Given a node, you can ask for children, siblings, and parents.
+ * - You can issue querySelector and querySelectorAll requests to find
+ * other elements.
+ * - Requests that return arbitrary nodes from the tree (like querySelector
+ * and querySelectorAll) will also return any nodes the client hasn't
+ * seen in order to have a complete set of parents.
+ *
+ * Once you have a NodeFront, you should be able to answer a few questions
+ * without further round trips, like the node's name, namespace/tagName,
+ * attributes, etc. Other questions (like a text node's full nodeValue)
+ * might require another round trip.
+ *
+ * The protocol guarantees that the client will always know the parent of
+ * any node that is returned by the server. This means that some requests
+ * (like querySelector) will include the extra nodes needed to satisfy this
+ * requirement. The client keeps track of this parent relationship, so the
+ * node fronts form a tree that is a subset of the actual DOM tree.
+ *
+ *
+ * We maintain this guarantee to support the ability to release subtrees on
+ * the client - when a node is disconnected from the DOM tree we want to be
+ * able to free the client objects for all the children nodes.
+ *
+ * So to be able to answer "all the children of a given node that we have
+ * seen on the client side", we guarantee that every time we've seen a node,
+ * we connect it up through its parents.
+ */
+
+const { Actor } = require("resource://devtools/shared/protocol.js");
+const {
+ inspectorSpec,
+} = require("resource://devtools/shared/specs/inspector.js");
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+const {
+ LongStringActor,
+} = require("resource://devtools/server/actors/string.js");
+
+loader.lazyRequireGetter(
+ this,
+ "InspectorActorUtils",
+ "resource://devtools/server/actors/inspector/utils.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "WalkerActor",
+ "resource://devtools/server/actors/inspector/walker.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "EyeDropper",
+ "resource://devtools/server/actors/highlighters/eye-dropper.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "PageStyleActor",
+ "resource://devtools/server/actors/page-style.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["CustomHighlighterActor", "isTypeRegistered", "HighlighterEnvironment"],
+ "resource://devtools/server/actors/highlighters.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "CompatibilityActor",
+ "resource://devtools/server/actors/compatibility/compatibility.js",
+ true
+);
+
+const SVG_NS = "http://www.w3.org/2000/svg";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+/**
+ * Server side of the inspector actor, which is used to create
+ * inspector-related actors, including the walker.
+ */
+class InspectorActor extends Actor {
+ constructor(conn, targetActor) {
+ super(conn, inspectorSpec);
+ this.targetActor = targetActor;
+
+ this._onColorPicked = this._onColorPicked.bind(this);
+ this._onColorPickCanceled = this._onColorPickCanceled.bind(this);
+ this.destroyEyeDropper = this.destroyEyeDropper.bind(this);
+ }
+
+ destroy() {
+ super.destroy();
+ this.destroyEyeDropper();
+
+ this._compatibility = null;
+ this._pageStylePromise = null;
+ this._walkerPromise = null;
+ this.walker = null;
+ this.targetActor = null;
+ }
+
+ get window() {
+ return this.targetActor.window;
+ }
+
+ getWalker(options = {}) {
+ if (this._walkerPromise) {
+ return this._walkerPromise;
+ }
+
+ this._walkerPromise = new Promise(resolve => {
+ const domReady = () => {
+ const targetActor = this.targetActor;
+ this.walker = new WalkerActor(this.conn, targetActor, options);
+ this.manage(this.walker);
+ this.walker.once("destroyed", () => {
+ this._walkerPromise = null;
+ this._pageStylePromise = null;
+ });
+ resolve(this.walker);
+ };
+
+ if (this.window.document.readyState === "loading") {
+ // Expose an abort controller for DOMContentLoaded to remove the
+ // listener unconditionally, even if the race hits the timeout.
+ const abortController = new AbortController();
+ Promise.race([
+ new Promise(r => {
+ this.window.addEventListener("DOMContentLoaded", r, {
+ capture: true,
+ once: true,
+ signal: abortController.signal,
+ });
+ }),
+ // The DOMContentLoaded event will never be emitted on documents stuck
+ // in the loading state, for instance if document.write was called
+ // without calling document.close.
+ // TODO: It is not clear why we are waiting for the event overall, see
+ // Bug 1766279 to actually stop listening to the event altogether.
+ new Promise(r => setTimeout(r, 500)),
+ ])
+ .then(domReady)
+ .finally(() => abortController.abort());
+ } else {
+ domReady();
+ }
+ });
+
+ return this._walkerPromise;
+ }
+
+ getPageStyle() {
+ if (this._pageStylePromise) {
+ return this._pageStylePromise;
+ }
+
+ this._pageStylePromise = this.getWalker().then(walker => {
+ const pageStyle = new PageStyleActor(this);
+ this.manage(pageStyle);
+ return pageStyle;
+ });
+ return this._pageStylePromise;
+ }
+
+ getCompatibility() {
+ if (this._compatibility) {
+ return this._compatibility;
+ }
+
+ this._compatibility = new CompatibilityActor(this);
+ this.manage(this._compatibility);
+ return this._compatibility;
+ }
+
+ /**
+ * If consumers need to display several highlighters at the same time or
+ * different types of highlighters, then this method should be used, passing
+ * the type name of the highlighter needed as argument.
+ * A new instance will be created everytime the method is called, so it's up
+ * to the consumer to release it when it is not needed anymore
+ *
+ * @param {String} type The type of highlighter to create
+ * @return {Highlighter} The highlighter actor instance or null if the
+ * typeName passed doesn't match any available highlighter
+ */
+ async getHighlighterByType(typeName) {
+ if (isTypeRegistered(typeName)) {
+ const highlighterActor = new CustomHighlighterActor(this, typeName);
+ if (highlighterActor.instance.isReady) {
+ await highlighterActor.instance.isReady;
+ }
+
+ return highlighterActor;
+ }
+ return null;
+ }
+
+ /**
+ * Get the node's image data if any (for canvas and img nodes).
+ * Returns an imageData object with the actual data being a LongStringActor
+ * and a size json object.
+ * The image data is transmitted as a base64 encoded png data-uri.
+ * The method rejects if the node isn't an image or if the image is missing
+ *
+ * Accepts a maxDim request parameter to resize images that are larger. This
+ * is important as the resizing occurs server-side so that image-data being
+ * transfered in the longstring back to the client will be that much smaller
+ */
+ getImageDataFromURL(url, maxDim) {
+ const img = new this.window.Image();
+ img.src = url;
+
+ // imageToImageData waits for the image to load.
+ return InspectorActorUtils.imageToImageData(img, maxDim).then(imageData => {
+ return {
+ data: new LongStringActor(this.conn, imageData.data),
+ size: imageData.size,
+ };
+ });
+ }
+
+ /**
+ * Resolve a URL to its absolute form, in the scope of a given content window.
+ * @param {String} url.
+ * @param {NodeActor} node If provided, the owner window of this node will be
+ * used to resolve the URL. Otherwise, the top-level content window will be
+ * used instead.
+ * @return {String} url.
+ */
+ resolveRelativeURL(url, node) {
+ const document = InspectorActorUtils.isNodeDead(node)
+ ? this.window.document
+ : InspectorActorUtils.nodeDocument(node.rawNode);
+
+ if (!document) {
+ return url;
+ }
+
+ const baseURI = Services.io.newURI(document.location.href);
+ return Services.io.newURI(url, null, baseURI).spec;
+ }
+
+ /**
+ * Create an instance of the eye-dropper highlighter and store it on this._eyeDropper.
+ * Note that for now, a new instance is created every time to deal with page navigation.
+ */
+ createEyeDropper() {
+ this.destroyEyeDropper();
+ this._highlighterEnv = new HighlighterEnvironment();
+ this._highlighterEnv.initFromTargetActor(this.targetActor);
+ this._eyeDropper = new EyeDropper(this._highlighterEnv);
+ return this._eyeDropper.isReady;
+ }
+
+ /**
+ * Destroy the current eye-dropper highlighter instance.
+ */
+ destroyEyeDropper() {
+ if (this._eyeDropper) {
+ this.cancelPickColorFromPage();
+ this._eyeDropper.destroy();
+ this._eyeDropper = null;
+ this._highlighterEnv.destroy();
+ this._highlighterEnv = null;
+ }
+ }
+
+ /**
+ * Pick a color from the page using the eye-dropper. This method doesn't return anything
+ * but will cause events to be sent to the front when a color is picked or when the user
+ * cancels the picker.
+ * @param {Object} options
+ */
+ async pickColorFromPage(options) {
+ await this.createEyeDropper();
+ this._eyeDropper.show(this.window.document.documentElement, options);
+ this._eyeDropper.once("selected", this._onColorPicked);
+ this._eyeDropper.once("canceled", this._onColorPickCanceled);
+ this.targetActor.once("will-navigate", this.destroyEyeDropper);
+ }
+
+ /**
+ * After the pickColorFromPage method is called, the only way to dismiss the eye-dropper
+ * highlighter is for the user to click in the page and select a color. If you need to
+ * dismiss the eye-dropper programatically instead, use this method.
+ */
+ cancelPickColorFromPage() {
+ if (this._eyeDropper) {
+ this._eyeDropper.hide();
+ this._eyeDropper.off("selected", this._onColorPicked);
+ this._eyeDropper.off("canceled", this._onColorPickCanceled);
+ this.targetActor.off("will-navigate", this.destroyEyeDropper);
+ }
+ }
+
+ /**
+ * Check if the current document supports highlighters using a canvasFrame anonymous
+ * content container.
+ * It is impossible to detect the feature programmatically as some document types simply
+ * don't render the canvasFrame without throwing any error.
+ */
+ supportsHighlighters() {
+ const doc = this.targetActor.window.document;
+ const ns = doc.documentElement.namespaceURI;
+
+ // XUL documents do not support insertAnonymousContent().
+ if (ns === XUL_NS) {
+ return false;
+ }
+
+ // SVG documents do not render the canvasFrame (see Bug 1157592).
+ if (ns === SVG_NS) {
+ return false;
+ }
+
+ return true;
+ }
+
+ _onColorPicked(color) {
+ this.emit("color-picked", color);
+ }
+
+ _onColorPickCanceled() {
+ this.emit("color-pick-canceled");
+ }
+}
+
+exports.InspectorActor = InspectorActor;