summaryrefslogtreecommitdiffstats
path: root/remote/marionette/json.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'remote/marionette/json.sys.mjs')
-rw-r--r--remote/marionette/json.sys.mjs491
1 files changed, 491 insertions, 0 deletions
diff --git a/remote/marionette/json.sys.mjs b/remote/marionette/json.sys.mjs
new file mode 100644
index 0000000000..bae1b99cdd
--- /dev/null
+++ b/remote/marionette/json.sys.mjs
@@ -0,0 +1,491 @@
+/* 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 { WebFrame, WebWindow } from "./web-reference.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ dom: "chrome://remote/content/shared/DOM.sys.mjs",
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+ Log: "chrome://remote/content/shared/Log.sys.mjs",
+ pprint: "chrome://remote/content/shared/Format.sys.mjs",
+ ShadowRoot: "chrome://remote/content/marionette/web-reference.sys.mjs",
+ TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
+ WebElement: "chrome://remote/content/marionette/web-reference.sys.mjs",
+ WebFrame: "chrome://remote/content/marionette/web-reference.sys.mjs",
+ WebReference: "chrome://remote/content/marionette/web-reference.sys.mjs",
+ WebWindow: "chrome://remote/content/marionette/web-reference.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "logger", () =>
+ lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
+);
+
+/** @namespace */
+export const json = {};
+
+/**
+ * Clone an object including collections.
+ *
+ * @param {object} value
+ * Object to be cloned.
+ * @param {Set} seen
+ * List of objects already processed.
+ * @param {Function} cloneAlgorithm
+ * The clone algorithm to invoke for individual list entries or object
+ * properties.
+ *
+ * @returns {object}
+ * The cloned object.
+ */
+function cloneObject(value, seen, cloneAlgorithm) {
+ // Only proceed with cloning an object if it hasn't been seen yet.
+ if (seen.has(value)) {
+ throw new lazy.error.JavaScriptError("Cyclic object value");
+ }
+ seen.add(value);
+
+ let result;
+
+ if (lazy.dom.isCollection(value)) {
+ result = [...value].map(entry => cloneAlgorithm(entry, seen));
+ } else {
+ // arbitrary objects
+ result = {};
+ for (let prop in value) {
+ try {
+ result[prop] = cloneAlgorithm(value[prop], seen);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED) {
+ lazy.logger.debug(`Skipping ${prop}: ${e.message}`);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ seen.delete(value);
+
+ return result;
+}
+
+/**
+ * Clone arbitrary objects to JSON-safe primitives that can be
+ * transported across processes and over the Marionette protocol.
+ *
+ * The marshaling rules are as follows:
+ *
+ * - Primitives are returned as is.
+ *
+ * - Collections, such as `Array`, `NodeList`, `HTMLCollection`
+ * et al. are transformed to arrays and then recursed.
+ *
+ * - Elements and ShadowRoots that are not known WebReference's are added to
+ * the `NodeCache`. For both the associated unique web reference identifier
+ * is returned.
+ *
+ * - Objects with custom JSON representations, i.e. if they have
+ * a callable `toJSON` function, are returned verbatim. This means
+ * their internal integrity _are not_ checked. Be careful.
+ *
+ * - If a cyclic references is detected a JavaScriptError is thrown.
+ *
+ * @param {object} value
+ * Object to be cloned.
+ * @param {NodeCache} nodeCache
+ * Node cache that holds already seen WebElement and ShadowRoot references.
+ *
+ * @returns {Object<Map<BrowsingContext, Array<string>, object>>}
+ * Object that contains a list of browsing contexts each with a list of
+ * shared ids for collected elements and shadow root nodes, and second the
+ * same object as provided by `value` with the WebDriver classic supported
+ * DOM nodes replaced by WebReference's.
+ *
+ * @throws {JavaScriptError}
+ * If an object contains cyclic references.
+ * @throws {StaleElementReferenceError}
+ * If the element has gone stale, indicating it is no longer
+ * attached to the DOM.
+ */
+json.clone = function (value, nodeCache) {
+ const seenNodeIds = new Map();
+ let hasSerializedWindows = false;
+
+ function cloneJSON(value, seen) {
+ if (seen === undefined) {
+ seen = new Set();
+ }
+
+ if ([undefined, null].includes(value)) {
+ return null;
+ }
+
+ const type = typeof value;
+
+ if (["boolean", "number", "string"].includes(type)) {
+ // Primitive values
+ return value;
+ }
+
+ // Evaluation of code might take place in mutable sandboxes, which are
+ // created to waive XRays by default. As such DOM nodes and windows
+ // have to be unwaived before accessing properties like "ownerGlobal"
+ // is possible.
+ //
+ // Until bug 1743788 is fixed there might be the possibility that more
+ // objects might need to be unwaived as well.
+ const isNode = Node.isInstance(value);
+ const isWindow = Window.isInstance(value);
+ if (isNode || isWindow) {
+ value = Cu.unwaiveXrays(value);
+ }
+
+ if (isNode && lazy.dom.isElement(value)) {
+ // Convert DOM elements to WebReference instances.
+
+ if (lazy.dom.isStale(value)) {
+ // Don't create a reference for stale elements.
+ throw new lazy.error.StaleElementReferenceError(
+ lazy.pprint`The element ${value} is no longer attached to the DOM`
+ );
+ }
+
+ const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds);
+
+ return lazy.WebReference.from(value, nodeRef).toJSON();
+ }
+
+ if (isNode && lazy.dom.isShadowRoot(value)) {
+ // Convert ShadowRoot instances to WebReference references.
+
+ if (lazy.dom.isDetached(value)) {
+ // Don't create a reference for detached shadow roots.
+ throw new lazy.error.DetachedShadowRootError(
+ lazy.pprint`The ShadowRoot ${value} is no longer attached to the DOM`
+ );
+ }
+
+ const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds);
+
+ return lazy.WebReference.from(value, nodeRef).toJSON();
+ }
+
+ if (isWindow) {
+ // Convert window instances to WebReference references.
+ let reference;
+
+ if (value.browsingContext.parent == null) {
+ reference = new WebWindow(value.browsingContext.browserId.toString());
+ hasSerializedWindows = true;
+ } else {
+ reference = new WebFrame(value.browsingContext.id.toString());
+ }
+
+ return reference.toJSON();
+ }
+
+ if (typeof value.toJSON == "function") {
+ // custom JSON representation
+ let unsafeJSON;
+ try {
+ unsafeJSON = value.toJSON();
+ } catch (e) {
+ throw new lazy.error.JavaScriptError(`toJSON() failed with: ${e}`);
+ }
+
+ return cloneJSON(unsafeJSON, seen);
+ }
+
+ // Collections and arbitrary objects
+ return cloneObject(value, seen, cloneJSON);
+ }
+
+ return {
+ seenNodeIds,
+ serializedValue: cloneJSON(value, new Set()),
+ hasSerializedWindows,
+ };
+};
+
+/**
+ * Deserialize an arbitrary object.
+ *
+ * @param {object} value
+ * Arbitrary object.
+ * @param {NodeCache} nodeCache
+ * Node cache that holds already seen WebElement and ShadowRoot references.
+ * @param {BrowsingContext} browsingContext
+ * The browsing context to check.
+ *
+ * @returns {object}
+ * Same object as provided by `value` with the WebDriver specific
+ * references replaced with real JavaScript objects.
+ *
+ * @throws {NoSuchElementError}
+ * If the WebElement reference has not been seen before.
+ * @throws {StaleElementReferenceError}
+ * If the element is stale, indicating it is no longer attached to the DOM.
+ */
+json.deserialize = function (value, nodeCache, browsingContext) {
+ function deserializeJSON(value, seen) {
+ if (seen === undefined) {
+ seen = new Set();
+ }
+
+ if (value === undefined || value === null) {
+ return value;
+ }
+
+ switch (typeof value) {
+ case "boolean":
+ case "number":
+ case "string":
+ default:
+ return value;
+
+ case "object":
+ if (lazy.WebReference.isReference(value)) {
+ // Create a WebReference based on the WebElement identifier.
+ const webRef = lazy.WebReference.fromJSON(value);
+
+ if (webRef instanceof lazy.ShadowRoot) {
+ return getKnownShadowRoot(browsingContext, webRef.uuid, nodeCache);
+ }
+
+ if (webRef instanceof lazy.WebElement) {
+ return getKnownElement(browsingContext, webRef.uuid, nodeCache);
+ }
+
+ if (webRef instanceof lazy.WebFrame) {
+ const browsingContext = BrowsingContext.get(webRef.uuid);
+
+ if (browsingContext === null || browsingContext.parent === null) {
+ throw new lazy.error.NoSuchWindowError(
+ `Unable to locate frame with id: ${webRef.uuid}`
+ );
+ }
+
+ return browsingContext.window;
+ }
+
+ if (webRef instanceof lazy.WebWindow) {
+ const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
+ webRef.uuid
+ );
+
+ if (browsingContext === null) {
+ throw new lazy.error.NoSuchWindowError(
+ `Unable to locate window with id: ${webRef.uuid}`
+ );
+ }
+
+ return browsingContext.window;
+ }
+ }
+
+ return cloneObject(value, seen, deserializeJSON);
+ }
+ }
+
+ return deserializeJSON(value, new Set());
+};
+
+/**
+ * Convert unique navigable ids to internal browser ids.
+ *
+ * @param {object} serializedData
+ * The data to process.
+ *
+ * @returns {object}
+ * The processed data.
+ */
+json.mapFromNavigableIds = function (serializedData) {
+ function _processData(data) {
+ if (lazy.WebReference.isReference(data)) {
+ const webRef = lazy.WebReference.fromJSON(data);
+
+ if (webRef instanceof lazy.WebWindow) {
+ const browser = lazy.TabManager.getBrowserById(webRef.uuid);
+ if (browser) {
+ webRef.uuid = browser?.browserId.toString();
+ data = webRef.toJSON();
+ }
+ }
+ } else if (typeof data === "object") {
+ for (const entry in data) {
+ data[entry] = _processData(data[entry]);
+ }
+ }
+
+ return data;
+ }
+
+ return _processData(serializedData);
+};
+
+/**
+ * Convert browser ids to unique navigable ids.
+ *
+ * @param {object} serializedData
+ * The data to process.
+ *
+ * @returns {object}
+ * The processed data.
+ */
+json.mapToNavigableIds = function (serializedData) {
+ function _processData(data) {
+ if (lazy.WebReference.isReference(data)) {
+ const webRef = lazy.WebReference.fromJSON(data);
+ if (webRef instanceof lazy.WebWindow) {
+ const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
+ webRef.uuid
+ );
+
+ webRef.uuid = lazy.TabManager.getIdForBrowsingContext(browsingContext);
+ data = webRef.toJSON();
+ }
+ } else if (typeof data == "object") {
+ for (const entry in data) {
+ data[entry] = _processData(data[entry]);
+ }
+ }
+
+ return data;
+ }
+
+ return _processData(serializedData);
+};
+
+/**
+ * Resolve element from specified web reference identifier.
+ *
+ * @param {BrowsingContext} browsingContext
+ * The browsing context to retrieve the element from.
+ * @param {string} nodeId
+ * The WebReference uuid for a DOM element.
+ * @param {NodeCache} nodeCache
+ * Node cache that holds already seen WebElement and ShadowRoot references.
+ *
+ * @returns {Element}
+ * The DOM element that the identifier was generated for.
+ *
+ * @throws {NoSuchElementError}
+ * If the element doesn't exist in the current browsing context.
+ * @throws {StaleElementReferenceError}
+ * If the element has gone stale, indicating its node document is no
+ * longer the active document or it is no longer attached to the DOM.
+ */
+export function getKnownElement(browsingContext, nodeId, nodeCache) {
+ if (!isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
+ throw new lazy.error.NoSuchElementError(
+ `The element with the reference ${nodeId} is not known in the current browsing context`,
+ { elementId: nodeId }
+ );
+ }
+
+ const node = nodeCache.getNode(browsingContext, nodeId);
+
+ // Ensure the node is of the correct Node type.
+ if (node !== null && !lazy.dom.isElement(node)) {
+ throw new lazy.error.NoSuchElementError(
+ `The element with the reference ${nodeId} is not of type HTMLElement`
+ );
+ }
+
+ // If null, which may be the case if the element has been unwrapped from a
+ // weak reference, it is always considered stale.
+ if (node === null || lazy.dom.isStale(node)) {
+ throw new lazy.error.StaleElementReferenceError(
+ `The element with the reference ${nodeId} ` +
+ "is stale; either its node document is not the active document, " +
+ "or it is no longer connected to the DOM"
+ );
+ }
+
+ return node;
+}
+
+/**
+ * Resolve ShadowRoot from specified web reference identifier.
+ *
+ * @param {BrowsingContext} browsingContext
+ * The browsing context to retrieve the shadow root from.
+ * @param {string} nodeId
+ * The WebReference uuid for a ShadowRoot.
+ * @param {NodeCache} nodeCache
+ * Node cache that holds already seen WebElement and ShadowRoot references.
+ *
+ * @returns {ShadowRoot}
+ * The ShadowRoot that the identifier was generated for.
+ *
+ * @throws {NoSuchShadowRootError}
+ * If the ShadowRoot doesn't exist in the current browsing context.
+ * @throws {DetachedShadowRootError}
+ * If the ShadowRoot is detached, indicating its node document is no
+ * longer the active document or it is no longer attached to the DOM.
+ */
+export function getKnownShadowRoot(browsingContext, nodeId, nodeCache) {
+ if (!isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
+ throw new lazy.error.NoSuchShadowRootError(
+ `The shadow root with the reference ${nodeId} is not known in the current browsing context`,
+ { shadowId: nodeId }
+ );
+ }
+
+ const node = nodeCache.getNode(browsingContext, nodeId);
+
+ // Ensure the node is of the correct Node type.
+ if (node !== null && !lazy.dom.isShadowRoot(node)) {
+ throw new lazy.error.NoSuchShadowRootError(
+ `The shadow root with the reference ${nodeId} is not of type ShadowRoot`
+ );
+ }
+
+ // If null, which may be the case if the element has been unwrapped from a
+ // weak reference, it is always considered stale.
+ if (node === null || lazy.dom.isDetached(node)) {
+ throw new lazy.error.DetachedShadowRootError(
+ `The shadow root with the reference ${nodeId} ` +
+ "is detached; either its node document is not the active document, " +
+ "or it is no longer connected to the DOM"
+ );
+ }
+
+ return node;
+}
+
+/**
+ * Determines if the node reference is known for the given browsing context.
+ *
+ * For WebDriver classic only nodes from the same browsing context are
+ * allowed to be accessed.
+ *
+ * @param {BrowsingContext} browsingContext
+ * The browsing context the element has to be part of.
+ * @param {ElementIdentifier} nodeId
+ * The WebElement reference identifier for a DOM element.
+ * @param {NodeCache} nodeCache
+ * Node cache that holds already seen node references.
+ *
+ * @returns {boolean}
+ * True if the element is known in the given browsing context.
+ */
+function isNodeReferenceKnown(browsingContext, nodeId, nodeCache) {
+ const nodeDetails = nodeCache.getReferenceDetails(nodeId);
+ if (nodeDetails === null) {
+ return false;
+ }
+
+ if (nodeDetails.isTopBrowsingContext) {
+ // As long as Navigables are not available any cross-group navigation will
+ // cause a swap of the current top-level browsing context. The only unique
+ // identifier in such a case is the browser id the top-level browsing
+ // context actually lives in.
+ return nodeDetails.browserId === browsingContext.browserId;
+ }
+
+ return nodeDetails.browsingContextId === browsingContext.id;
+}