Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / remote / marionette / json.sys.mjs
blob49c5ab6836bd498c80f790f45319780559f4a250
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";
7 const lazy = {};
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",
20 });
22 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
23   lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
26 /** @namespace */
27 export const json = {};
29 /**
30  * Clone an object including collections.
31  *
32  * @param {object} value
33  *     Object to be cloned.
34  * @param {Set} seen
35  *     List of objects already processed.
36  * @param {Function} cloneAlgorithm
37  *     The clone algorithm to invoke for individual list entries or object
38  *     properties.
39  *
40  * @returns {object}
41  *     The cloned object.
42  */
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");
47   }
48   seen.add(value);
50   let result;
52   if (lazy.dom.isCollection(value)) {
53     result = [...value].map(entry => cloneAlgorithm(entry, seen));
54   } else {
55     // arbitrary objects
56     result = {};
57     for (let prop in value) {
58       try {
59         result[prop] = cloneAlgorithm(value[prop], seen);
60       } catch (e) {
61         if (e.result == Cr.NS_ERROR_NOT_IMPLEMENTED) {
62           lazy.logger.debug(`Skipping ${prop}: ${e.message}`);
63         } else {
64           throw e;
65         }
66       }
67     }
68   }
70   seen.delete(value);
72   return result;
75 /**
76  * Clone arbitrary objects to JSON-safe primitives that can be
77  * transported across processes and over the Marionette protocol.
78  *
79  * The marshaling rules are as follows:
80  *
81  * - Primitives are returned as is.
82  *
83  * - Collections, such as `Array`, `NodeList`, `HTMLCollection`
84  *   et al. are transformed to arrays and then recursed.
85  *
86  * - Elements and ShadowRoots that are not known WebReference's are added to
87  *   the `NodeCache`. For both the associated unique web reference identifier
88  *   is returned.
89  *
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.
93  *
94  * - If a cyclic references is detected a JavaScriptError is thrown.
95  *
96  * @param {object} value
97  *     Object to be cloned.
98  * @param {NodeCache} nodeCache
99  *     Node cache that holds already seen WebElement and ShadowRoot references.
101  * @returns {{
102  *   seenNodeIds: Map<BrowsingContext, string[]>,
103  *   serializedValue: any,
104  *   hasSerializedWindows: boolean
105  * }}
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.
116  */
117 json.clone = function (value, nodeCache) {
118   const seenNodeIds = new Map();
119   let hasSerializedWindows = false;
121   function cloneJSON(value, seen) {
122     if (seen === undefined) {
123       seen = new Set();
124     }
126     if ([undefined, null].includes(value)) {
127       return null;
128     }
130     const type = typeof value;
132     if (["boolean", "number", "string"].includes(type)) {
133       // Primitive values
134       return value;
135     }
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"
140     // is possible.
141     //
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);
148     }
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`
157         );
158       }
160       const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds);
162       return lazy.WebReference.from(value, nodeRef).toJSON();
163     }
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`
172         );
173       }
175       const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds);
177       return lazy.WebReference.from(value, nodeRef).toJSON();
178     }
180     if (isWindow) {
181       // Convert window instances to WebReference references.
182       let reference;
184       if (value.browsingContext.parent == null) {
185         reference = new WebWindow(value.browsingContext.browserId.toString());
186         hasSerializedWindows = true;
187       } else {
188         reference = new WebFrame(value.browsingContext.id.toString());
189       }
191       return reference.toJSON();
192     }
194     if (typeof value.toJSON == "function") {
195       // custom JSON representation
196       let unsafeJSON;
197       try {
198         unsafeJSON = value.toJSON();
199       } catch (e) {
200         throw new lazy.error.JavaScriptError(`toJSON() failed with: ${e}`);
201       }
203       return cloneJSON(unsafeJSON, seen);
204     }
206     // Collections and arbitrary objects
207     return cloneObject(value, seen, cloneJSON);
208   }
210   return {
211     seenNodeIds,
212     serializedValue: cloneJSON(value, new Set()),
213     hasSerializedWindows,
214   };
218  * Deserialize an arbitrary object.
220  * @param {object} value
221  *     Arbitrary object.
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.
227  * @returns {object}
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.
235  */
236 json.deserialize = function (value, nodeCache, browsingContext) {
237   function deserializeJSON(value, seen) {
238     if (seen === undefined) {
239       seen = new Set();
240     }
242     if (value === undefined || value === null) {
243       return value;
244     }
246     switch (typeof value) {
247       case "boolean":
248       case "number":
249       case "string":
250       default:
251         return value;
253       case "object":
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);
260           }
262           if (webRef instanceof lazy.WebElement) {
263             return getKnownElement(browsingContext, webRef.uuid, nodeCache);
264           }
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}`
272               );
273             }
275             return browsingContext.window;
276           }
278           if (webRef instanceof lazy.WebWindow) {
279             const browsingContext = BrowsingContext.getCurrentTopByBrowserId(
280               webRef.uuid
281             );
283             if (browsingContext === null) {
284               throw new lazy.error.NoSuchWindowError(
285                 `Unable to locate window with id: ${webRef.uuid}`
286               );
287             }
289             return browsingContext.window;
290           }
291         }
293         return cloneObject(value, seen, deserializeJSON);
294     }
295   }
297   return deserializeJSON(value, new Set());
301  * Convert unique navigable ids to internal browser ids.
303  * @param {object} serializedData
304  *     The data to process.
306  * @returns {object}
307  *     The processed data.
308  */
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);
316         if (browser) {
317           webRef.uuid = browser?.browserId.toString();
318           data = webRef.toJSON();
319         }
320       }
321     } else if (typeof data === "object") {
322       for (const entry in data) {
323         data[entry] = _processData(data[entry]);
324       }
325     }
327     return data;
328   }
330   return _processData(serializedData);
334  * Convert browser ids to unique navigable ids.
336  * @param {object} serializedData
337  *     The data to process.
339  * @returns {object}
340  *     The processed data.
341  */
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(
348           webRef.uuid
349         );
351         webRef.uuid = lazy.TabManager.getIdForBrowsingContext(browsingContext);
352         data = webRef.toJSON();
353       }
354     } else if (typeof data == "object") {
355       for (const entry in data) {
356         data[entry] = _processData(data[entry]);
357       }
358     }
360     return data;
361   }
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.
376  * @returns {Element}
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.
384  */
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 }
390     );
391   }
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`
399     );
400   }
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"
409     );
410   }
412   return node;
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.
433  */
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`,
438       { shadowId: nodeId }
439     );
440   }
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`
448     );
449   }
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"
458     );
459   }
461   return node;
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.
477  * @returns {boolean}
478  *     True if the element is known in the given browsing context.
479  */
480 function isNodeReferenceKnown(browsingContext, nodeId, nodeCache) {
481   const nodeDetails = nodeCache.getReferenceDetails(nodeId);
482   if (nodeDetails === null) {
483     return false;
484   }
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;
492   }
494   return nodeDetails.browsingContextId === browsingContext.id;