1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { WebFrame, WebWindow } from "./web-reference.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 dom: "chrome://remote/content/shared/DOM.sys.mjs",
11 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
12 Log: "chrome://remote/content/shared/Log.sys.mjs",
13 pprint: "chrome://remote/content/shared/Format.sys.mjs",
14 ShadowRoot: "chrome://remote/content/marionette/web-reference.sys.mjs",
15 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
16 WebElement: "chrome://remote/content/marionette/web-reference.sys.mjs",
17 WebFrame: "chrome://remote/content/marionette/web-reference.sys.mjs",
18 WebReference: "chrome://remote/content/marionette/web-reference.sys.mjs",
19 WebWindow: "chrome://remote/content/marionette/web-reference.sys.mjs",
22 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
23 lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
27 export const json = {};
30 * Clone an object including collections.
32 * @param {object} value
33 * Object to be cloned.
35 * List of objects already processed.
36 * @param {Function} cloneAlgorithm
37 * The clone algorithm to invoke for individual list entries or object
43 function cloneObject(value, seen, cloneAlgorithm) {
44 // Only proceed with cloning an object if it hasn't been seen yet.
45 if (seen.has(value)) {
46 throw new lazy.error.JavaScriptError("Cyclic object value");
52 if (lazy.dom.isCollection(value)) {
53 result = [...value].map(entry => cloneAlgorithm(entry, seen));
57 for (let prop in value) {
59 result[prop] = cloneAlgorithm(value[prop], seen);
61 if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED) {
62 lazy.logger.debug(`Skipping ${prop}: ${e.message}`);
76 * Clone arbitrary objects to JSON-safe primitives that can be
77 * transported across processes and over the Marionette protocol.
79 * The marshaling rules are as follows:
81 * - Primitives are returned as is.
83 * - Collections, such as `Array`, `NodeList`, `HTMLCollection`
84 * et al. are transformed to arrays and then recursed.
86 * - Elements and ShadowRoots that are not known WebReference's are added to
87 * the `NodeCache`. For both the associated unique web reference identifier
90 * - Objects with custom JSON representations, i.e. if they have
91 * a callable `toJSON` function, are returned verbatim. This means
92 * their internal integrity _are not_ checked. Be careful.
94 * - If a cyclic references is detected a JavaScriptError is thrown.
96 * @param {object} value
97 * Object to be cloned.
98 * @param {NodeCache} nodeCache
99 * Node cache that holds already seen WebElement and ShadowRoot references.
102 * seenNodeIds: Map<BrowsingContext, string[]>,
103 * serializedValue: any,
104 * hasSerializedWindows: boolean
106 * Object that contains a list of browsing contexts each with a list of
107 * shared ids for collected elements and shadow root nodes, and second the
108 * same object as provided by `value` with the WebDriver classic supported
109 * DOM nodes replaced by WebReference's.
111 * @throws {JavaScriptError}
112 * If an object contains cyclic references.
113 * @throws {StaleElementReferenceError}
114 * If the element has gone stale, indicating it is no longer
115 * attached to the DOM.
117 json.clone = function (value, nodeCache) {
118 const seenNodeIds = new Map();
119 let hasSerializedWindows = false;
121 function cloneJSON(value, seen) {
122 if (seen === undefined) {
126 if ([undefined, null].includes(value)) {
130 const type = typeof value;
132 if (["boolean", "number", "string"].includes(type)) {
137 // Evaluation of code might take place in mutable sandboxes, which are
138 // created to waive XRays by default. As such DOM nodes and windows
139 // have to be unwaived before accessing properties like "ownerGlobal"
142 // Until bug 1743788 is fixed there might be the possibility that more
143 // objects might need to be unwaived as well.
144 const isNode = Node.isInstance(value);
145 const isWindow = Window.isInstance(value);
146 if (isNode || isWindow) {
147 value = Cu.unwaiveXrays(value);
150 if (isNode && lazy.dom.isElement(value)) {
151 // Convert DOM elements to WebReference instances.
153 if (lazy.dom.isStale(value)) {
154 // Don't create a reference for stale elements.
155 throw new lazy.error.StaleElementReferenceError(
156 lazy.pprint`The element ${value} is no longer attached to the DOM`
160 const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds);
162 return lazy.WebReference.from(value, nodeRef).toJSON();
165 if (isNode && lazy.dom.isShadowRoot(value)) {
166 // Convert ShadowRoot instances to WebReference references.
168 if (lazy.dom.isDetached(value)) {
169 // Don't create a reference for detached shadow roots.
170 throw new lazy.error.DetachedShadowRootError(
171 lazy.pprint`The ShadowRoot ${value} is no longer attached to the DOM`
175 const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds);
177 return lazy.WebReference.from(value, nodeRef).toJSON();
181 // Convert window instances to WebReference references.
184 if (value.browsingContext.parent == null) {
185 reference = new WebWindow(value.browsingContext.browserId.toString());
186 hasSerializedWindows = true;
188 reference = new WebFrame(value.browsingContext.id.toString());
191 return reference.toJSON();
194 if (typeof value.toJSON == "function") {
195 // custom JSON representation
198 unsafeJSON = value.toJSON();
200 throw new lazy.error.JavaScriptError(`toJSON() failed with: ${e}`);
203 return cloneJSON(unsafeJSON, seen);
206 // Collections and arbitrary objects
207 return cloneObject(value, seen, cloneJSON);
212 serializedValue: cloneJSON(value, new Set()),
213 hasSerializedWindows,
218 * Deserialize an arbitrary object.
220 * @param {object} value
222 * @param {NodeCache} nodeCache
223 * Node cache that holds already seen WebElement and ShadowRoot references.
224 * @param {BrowsingContext} browsingContext
225 * The browsing context to check.
228 * Same object as provided by `value` with the WebDriver specific
229 * references replaced with real JavaScript objects.
231 * @throws {NoSuchElementError}
232 * If the WebElement reference has not been seen before.
233 * @throws {StaleElementReferenceError}
234 * If the element is stale, indicating it is no longer attached to the DOM.
236 json.deserialize = function (value, nodeCache, browsingContext) {
237 function deserializeJSON(value, seen) {
238 if (seen === undefined) {
242 if (value === undefined || value === null) {
246 switch (typeof value) {
254 if (lazy.WebReference.isReference(value)) {
255 // Create a WebReference based on the WebElement identifier.
256 const webRef = lazy.WebReference.fromJSON(value);
258 if (webRef instanceof lazy.ShadowRoot) {
259 return getKnownShadowRoot(browsingContext, webRef.uuid, nodeCache);
262 if (webRef instanceof lazy.WebElement) {
263 return getKnownElement(browsingContext, webRef.uuid, nodeCache);
266 if (webRef instanceof lazy.WebFrame) {
267 const browsingContext = BrowsingContext.get(webRef.uuid);
269 if (browsingContext === null || browsingContext.parent === null) {
270 throw new lazy.error.NoSuchWindowError(
271 `Unable to locate frame with id: ${webRef.uuid}`
275 return browsingContext.window;
278 if (webRef instanceof lazy.WebWindow) {
279 const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
283 if (browsingContext === null) {
284 throw new lazy.error.NoSuchWindowError(
285 `Unable to locate window with id: ${webRef.uuid}`
289 return browsingContext.window;
293 return cloneObject(value, seen, deserializeJSON);
297 return deserializeJSON(value, new Set());
301 * Convert unique navigable ids to internal browser ids.
303 * @param {object} serializedData
304 * The data to process.
307 * The processed data.
309 json.mapFromNavigableIds = function (serializedData) {
310 function _processData(data) {
311 if (lazy.WebReference.isReference(data)) {
312 const webRef = lazy.WebReference.fromJSON(data);
314 if (webRef instanceof lazy.WebWindow) {
315 const browser = lazy.TabManager.getBrowserById(webRef.uuid);
317 webRef.uuid = browser?.browserId.toString();
318 data = webRef.toJSON();
321 } else if (typeof data === "object") {
322 for (const entry in data) {
323 data[entry] = _processData(data[entry]);
330 return _processData(serializedData);
334 * Convert browser ids to unique navigable ids.
336 * @param {object} serializedData
337 * The data to process.
340 * The processed data.
342 json.mapToNavigableIds = function (serializedData) {
343 function _processData(data) {
344 if (lazy.WebReference.isReference(data)) {
345 const webRef = lazy.WebReference.fromJSON(data);
346 if (webRef instanceof lazy.WebWindow) {
347 const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
351 webRef.uuid = lazy.TabManager.getIdForBrowsingContext(browsingContext);
352 data = webRef.toJSON();
354 } else if (typeof data == "object") {
355 for (const entry in data) {
356 data[entry] = _processData(data[entry]);
363 return _processData(serializedData);
367 * Resolve element from specified web reference identifier.
369 * @param {BrowsingContext} browsingContext
370 * The browsing context to retrieve the element from.
371 * @param {string} nodeId
372 * The WebReference uuid for a DOM element.
373 * @param {NodeCache} nodeCache
374 * Node cache that holds already seen WebElement and ShadowRoot references.
377 * The DOM element that the identifier was generated for.
379 * @throws {NoSuchElementError}
380 * If the element doesn't exist in the current browsing context.
381 * @throws {StaleElementReferenceError}
382 * If the element has gone stale, indicating its node document is no
383 * longer the active document or it is no longer attached to the DOM.
385 export function getKnownElement(browsingContext, nodeId, nodeCache) {
386 if (!isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
387 throw new lazy.error.NoSuchElementError(
388 `The element with the reference ${nodeId} is not known in the current browsing context`,
389 { elementId: nodeId }
393 const node = nodeCache.getNode(browsingContext, nodeId);
395 // Ensure the node is of the correct Node type.
396 if (node !== null && !lazy.dom.isElement(node)) {
397 throw new lazy.error.NoSuchElementError(
398 `The element with the reference ${nodeId} is not of type HTMLElement`
402 // If null, which may be the case if the element has been unwrapped from a
403 // weak reference, it is always considered stale.
404 if (node === null || lazy.dom.isStale(node)) {
405 throw new lazy.error.StaleElementReferenceError(
406 `The element with the reference ${nodeId} ` +
407 "is stale; either its node document is not the active document, " +
408 "or it is no longer connected to the DOM"
416 * Resolve ShadowRoot from specified web reference identifier.
418 * @param {BrowsingContext} browsingContext
419 * The browsing context to retrieve the shadow root from.
420 * @param {string} nodeId
421 * The WebReference uuid for a ShadowRoot.
422 * @param {NodeCache} nodeCache
423 * Node cache that holds already seen WebElement and ShadowRoot references.
425 * @returns {ShadowRoot}
426 * The ShadowRoot that the identifier was generated for.
428 * @throws {NoSuchShadowRootError}
429 * If the ShadowRoot doesn't exist in the current browsing context.
430 * @throws {DetachedShadowRootError}
431 * If the ShadowRoot is detached, indicating its node document is no
432 * longer the active document or it is no longer attached to the DOM.
434 export function getKnownShadowRoot(browsingContext, nodeId, nodeCache) {
435 if (!isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) {
436 throw new lazy.error.NoSuchShadowRootError(
437 `The shadow root with the reference ${nodeId} is not known in the current browsing context`,
442 const node = nodeCache.getNode(browsingContext, nodeId);
444 // Ensure the node is of the correct Node type.
445 if (node !== null && !lazy.dom.isShadowRoot(node)) {
446 throw new lazy.error.NoSuchShadowRootError(
447 `The shadow root with the reference ${nodeId} is not of type ShadowRoot`
451 // If null, which may be the case if the element has been unwrapped from a
452 // weak reference, it is always considered stale.
453 if (node === null || lazy.dom.isDetached(node)) {
454 throw new lazy.error.DetachedShadowRootError(
455 `The shadow root with the reference ${nodeId} ` +
456 "is detached; either its node document is not the active document, " +
457 "or it is no longer connected to the DOM"
465 * Determines if the node reference is known for the given browsing context.
467 * For WebDriver classic only nodes from the same browsing context are
468 * allowed to be accessed.
470 * @param {BrowsingContext} browsingContext
471 * The browsing context the element has to be part of.
472 * @param {ElementIdentifier} nodeId
473 * The WebElement reference identifier for a DOM element.
474 * @param {NodeCache} nodeCache
475 * Node cache that holds already seen node references.
478 * True if the element is known in the given browsing context.
480 function isNodeReferenceKnown(browsingContext, nodeId, nodeCache) {
481 const nodeDetails = nodeCache.getReferenceDetails(nodeId);
482 if (nodeDetails === null) {
486 if (nodeDetails.isTopBrowsingContext) {
487 // As long as Navigables are not available any cross-group navigation will
488 // cause a swap of the current top-level browsing context. The only unique
489 // identifier in such a case is the browser id the top-level browsing
490 // context actually lives in.
491 return nodeDetails.browserId === browsingContext.browserId;
494 return nodeDetails.browsingContextId === browsingContext.id;