tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }