json.sys.mjs (16120B)
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/. */ 4 5 import { WebFrame, WebWindow } from "./web-reference.sys.mjs"; 6 7 const lazy = {}; 8 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 NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", 14 pprint: "chrome://remote/content/shared/Format.sys.mjs", 15 ShadowRoot: "chrome://remote/content/marionette/web-reference.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 }); 21 22 ChromeUtils.defineLazyGetter(lazy, "logger", () => 23 lazy.Log.get(lazy.Log.TYPES.MARIONETTE) 24 ); 25 26 /** @namespace */ 27 export const json = {}; 28 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 if (seen.has(value)) { 45 // Only proceed with cloning an object if it hasn't been seen yet. 46 throw new lazy.error.JavaScriptError(`Cyclic object value: ${value}`); 47 } 48 seen.add(value); 49 50 let result; 51 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 } 69 70 seen.delete(value); 71 72 return result; 73 } 74 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. 100 * 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. 110 * 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; 120 121 function cloneJSON(value, seen) { 122 if (seen === undefined) { 123 seen = new Set(); 124 } 125 126 if ([undefined, null].includes(value)) { 127 return null; 128 } 129 130 const type = typeof value; 131 132 if (["boolean", "number", "string"].includes(type)) { 133 // Primitive values 134 return value; 135 } 136 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 } 149 150 if (isNode && lazy.dom.isElement(value)) { 151 // Convert DOM elements to WebReference instances. 152 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 } 159 160 const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds); 161 162 return lazy.WebReference.from(value, nodeRef).toJSON(); 163 } 164 165 if (isNode && lazy.dom.isShadowRoot(value)) { 166 // Convert ShadowRoot instances to WebReference references. 167 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 } 174 175 const nodeRef = nodeCache.getOrCreateNodeReference(value, seenNodeIds); 176 177 return lazy.WebReference.from(value, nodeRef).toJSON(); 178 } 179 180 if (isWindow) { 181 let reference; 182 183 // Convert window instances to serialized WebWindow or WebFrame references. 184 // Because the NavigableManager is only available in the parent process, 185 // we need to pass the actual id of the browsing context. 186 if (value.browsingContext.parent == null) { 187 reference = new WebWindow(value.browsingContext.id.toString()); 188 } else { 189 reference = new WebFrame(value.browsingContext.id.toString()); 190 } 191 192 hasSerializedWindows = true; 193 194 return reference.toJSON(); 195 } 196 197 if (typeof value.toJSON == "function") { 198 // custom JSON representation 199 let unsafeJSON; 200 try { 201 unsafeJSON = value.toJSON(); 202 } catch (e) { 203 throw new lazy.error.JavaScriptError(`toJSON() failed with: ${e}`); 204 } 205 206 return cloneJSON(unsafeJSON, seen); 207 } 208 209 // Collections and arbitrary objects 210 return cloneObject(value, seen, cloneJSON); 211 } 212 213 return { 214 seenNodeIds, 215 serializedValue: cloneJSON(value, new Set()), 216 hasSerializedWindows, 217 }; 218 }; 219 220 /** 221 * Deserialize an arbitrary object. 222 * 223 * @param {object} value 224 * Arbitrary object. 225 * @param {NodeCache} nodeCache 226 * Node cache that holds already seen WebElement and ShadowRoot references. 227 * @param {BrowsingContext} browsingContext 228 * The browsing context to check. 229 * 230 * @returns {object} 231 * Same object as provided by `value` with the WebDriver specific 232 * references replaced with real JavaScript objects. 233 * 234 * @throws {NoSuchElementError} 235 * If the WebElement reference has not been seen before. 236 * @throws {NoSuchFrameError} 237 * Child browsing context has been discarded. 238 * @throws {NoSuchWindowError} 239 * Top-level browsing context has been discarded. 240 * @throws {StaleElementReferenceError} 241 * If the element is stale, indicating it is no longer attached to the DOM. 242 */ 243 json.deserialize = function (value, nodeCache, browsingContext) { 244 function deserializeJSON(value, seen) { 245 if (seen === undefined) { 246 seen = new Set(); 247 } 248 249 if (value === undefined || value === null) { 250 return value; 251 } 252 253 switch (typeof value) { 254 case "boolean": 255 case "number": 256 case "string": 257 default: 258 return value; 259 260 case "object": 261 if (lazy.WebReference.isReference(value)) { 262 // Create a WebReference based on the WebElement identifier. 263 const webRef = lazy.WebReference.fromJSON(value); 264 265 if (webRef instanceof lazy.ShadowRoot) { 266 return getKnownShadowRoot(browsingContext, webRef.uuid, nodeCache); 267 } 268 269 if (webRef instanceof lazy.WebElement) { 270 return getKnownElement(browsingContext, webRef.uuid, nodeCache); 271 } 272 273 if (webRef instanceof lazy.WebFrame) { 274 const frameContext = BrowsingContext.get(webRef.uuid); 275 276 if (frameContext === null || frameContext.parent === null) { 277 throw new lazy.error.NoSuchFrameError( 278 `Unable to locate frame with id: ${webRef.uuid}` 279 ); 280 } 281 282 return frameContext.window; 283 } 284 285 if (webRef instanceof lazy.WebWindow) { 286 const windowContext = BrowsingContext.get(webRef.uuid); 287 288 if (windowContext === null || windowContext.parent !== null) { 289 throw new lazy.error.NoSuchWindowError( 290 `Unable to locate window with id: ${webRef.uuid}` 291 ); 292 } 293 294 return windowContext.window; 295 } 296 } 297 298 return cloneObject(value, seen, deserializeJSON); 299 } 300 } 301 302 return deserializeJSON(value, new Set()); 303 }; 304 305 /** 306 * Convert unique navigable ids for windows and frames to browsing context ids. 307 * 308 * @param {object} serializedData 309 * The data to process. 310 * 311 * @returns {object} 312 * The processed data. 313 */ 314 json.mapFromNavigableIds = function (serializedData) { 315 function _processData(data) { 316 if (lazy.WebReference.isReference(data)) { 317 const webRef = lazy.WebReference.fromJSON(data); 318 319 if (webRef instanceof lazy.WebFrame || webRef instanceof lazy.WebWindow) { 320 const context = lazy.NavigableManager.getBrowsingContextById( 321 webRef.uuid 322 ); 323 324 if (context) { 325 webRef.uuid = context.id.toString(); 326 data = webRef.toJSON(); 327 } 328 } 329 } else if (typeof data === "object") { 330 for (const entry in data) { 331 data[entry] = _processData(data[entry]); 332 } 333 } 334 335 return data; 336 } 337 338 return _processData(serializedData); 339 }; 340 341 /** 342 * Convert browsing context ids for windows and frames to unique navigable ids. 343 * 344 * @param {object} serializedData 345 * The data to process. 346 * 347 * @returns {object} 348 * The processed data. 349 */ 350 json.mapToNavigableIds = function (serializedData) { 351 function _processData(data) { 352 if (lazy.WebReference.isReference(data)) { 353 const webRef = lazy.WebReference.fromJSON(data); 354 if (webRef instanceof lazy.WebWindow || webRef instanceof lazy.WebFrame) { 355 const browsingContext = BrowsingContext.get(webRef.uuid); 356 357 webRef.uuid = 358 lazy.NavigableManager.getIdForBrowsingContext(browsingContext); 359 data = webRef.toJSON(); 360 } 361 } else if (typeof data == "object") { 362 for (const entry in data) { 363 data[entry] = _processData(data[entry]); 364 } 365 } 366 367 return data; 368 } 369 370 return _processData(serializedData); 371 }; 372 373 /** 374 * Resolve element from specified web reference identifier. 375 * 376 * @param {BrowsingContext} browsingContext 377 * The browsing context to retrieve the element from. 378 * @param {string} nodeId 379 * The WebReference uuid for a DOM element. 380 * @param {NodeCache} nodeCache 381 * Node cache that holds already seen WebElement and ShadowRoot references. 382 * 383 * @returns {Element} 384 * The DOM element that the identifier was generated for. 385 * 386 * @throws {NoSuchElementError} 387 * If the element doesn't exist in the current browsing context. 388 * @throws {StaleElementReferenceError} 389 * If the element has gone stale, indicating its node document is no 390 * longer the active document or it is no longer attached to the DOM. 391 */ 392 export function getKnownElement(browsingContext, nodeId, nodeCache) { 393 if (!isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) { 394 throw new lazy.error.NoSuchElementError( 395 `The element with the reference ${nodeId} is not known in the current browsing context`, 396 { elementId: nodeId } 397 ); 398 } 399 400 const node = nodeCache.getNode(browsingContext, nodeId); 401 402 // Ensure the node is of the correct Node type. 403 if (node !== null && !lazy.dom.isElement(node)) { 404 throw new lazy.error.NoSuchElementError( 405 `The element with the reference ${nodeId} is not of type HTMLElement` 406 ); 407 } 408 409 // If null, which may be the case if the element has been unwrapped from a 410 // weak reference, it is always considered stale. 411 if (node === null || lazy.dom.isStale(node)) { 412 throw new lazy.error.StaleElementReferenceError( 413 `The element with the reference ${nodeId} ` + 414 "is stale; either its node document is not the active document, " + 415 "or it is no longer connected to the DOM" 416 ); 417 } 418 419 return node; 420 } 421 422 /** 423 * Resolve ShadowRoot from specified web reference identifier. 424 * 425 * @param {BrowsingContext} browsingContext 426 * The browsing context to retrieve the shadow root from. 427 * @param {string} nodeId 428 * The WebReference uuid for a ShadowRoot. 429 * @param {NodeCache} nodeCache 430 * Node cache that holds already seen WebElement and ShadowRoot references. 431 * 432 * @returns {ShadowRoot} 433 * The ShadowRoot that the identifier was generated for. 434 * 435 * @throws {NoSuchShadowRootError} 436 * If the ShadowRoot doesn't exist in the current browsing context. 437 * @throws {DetachedShadowRootError} 438 * If the ShadowRoot is detached, indicating its node document is no 439 * longer the active document or it is no longer attached to the DOM. 440 */ 441 export function getKnownShadowRoot(browsingContext, nodeId, nodeCache) { 442 if (!isNodeReferenceKnown(browsingContext, nodeId, nodeCache)) { 443 throw new lazy.error.NoSuchShadowRootError( 444 `The shadow root with the reference ${nodeId} is not known in the current browsing context`, 445 { shadowId: nodeId } 446 ); 447 } 448 449 const node = nodeCache.getNode(browsingContext, nodeId); 450 451 // Ensure the node is of the correct Node type. 452 if (node !== null && !lazy.dom.isShadowRoot(node)) { 453 throw new lazy.error.NoSuchShadowRootError( 454 `The shadow root with the reference ${nodeId} is not of type ShadowRoot` 455 ); 456 } 457 458 // If null, which may be the case if the element has been unwrapped from a 459 // weak reference, it is always considered stale. 460 if (node === null || lazy.dom.isDetached(node)) { 461 throw new lazy.error.DetachedShadowRootError( 462 `The shadow root with the reference ${nodeId} ` + 463 "is detached; either its node document is not the active document, " + 464 "or it is no longer connected to the DOM" 465 ); 466 } 467 468 return node; 469 } 470 471 /** 472 * Determines if the node reference is known for the given browsing context. 473 * 474 * For WebDriver classic only nodes from the same browsing context are 475 * allowed to be accessed. 476 * 477 * @param {BrowsingContext} browsingContext 478 * The browsing context the element has to be part of. 479 * @param {ElementIdentifier} nodeId 480 * The WebElement reference identifier for a DOM element. 481 * @param {NodeCache} nodeCache 482 * Node cache that holds already seen node references. 483 * 484 * @returns {boolean} 485 * True if the element is known in the given browsing context. 486 */ 487 function isNodeReferenceKnown(browsingContext, nodeId, nodeCache) { 488 const nodeDetails = nodeCache.getReferenceDetails(nodeId); 489 if (nodeDetails === null) { 490 return false; 491 } 492 493 if (nodeDetails.isTopBrowsingContext) { 494 // As long as Navigables are not available any cross-group navigation will 495 // cause a swap of the current top-level browsing context. The only unique 496 // identifier in such a case is the browser id the top-level browsing 497 // context actually lives in. 498 return nodeDetails.browserId === browsingContext.browserId; 499 } 500 501 return nodeDetails.browsingContextId === browsingContext.id; 502 }