chromium/chrome/test/chromedriver/js/call_function.js

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * Enum for WebDriver status codes.
 * @enum {number}
 */
const StatusCode = {
  STALE_ELEMENT_REFERENCE: 10,
  JAVA_SCRIPT_ERROR: 17,
  NO_SUCH_SHADOW_ROOT: 65,
  DETACHED_SHADOW_ROOT: 66
};

/**
 * Enum for node types.
 * @enum {number}
 */
const NodeType = {
  ELEMENT: 1,
  DOCUMENT: 9,
};

/**
 * Dictionary key to use for holding an element ID.
 * @const
 * @type {string}
 */
var ELEMENT_KEY = 'ELEMENT';

/**
 * Dictionary key to use for holding a shadow element ID.
 * @const
 * @type {string}
 */
const SHADOW_ROOT_KEY = 'shadow-6066-11e4-a52e-4f735466cecf';
const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';

/**
 * True if using W3C Element references.
 * @const
 * @type {boolean}
 */
var w3cEnabled = false;

/**
 * True if shadow dom is enabled.
 * @const
 * @type {boolean}
 */
const SHADOW_DOM_ENABLED = typeof ShadowRoot === 'function';

/**
 * Constructs new error to be thrown with given code and message.
 * @param {string} message Message reported to user.
 * @param {StatusCode} code StatusCode for error.
 * @return {!Error} Error object that can be thrown.
 */
function newError(message, code) {
  const error = new Error(message);
  error.code = code;
  return error;
}

function isNodeReachable(node) {
  const nodeRoot = getNodeRootThroughAnyShadows(node);
  return (nodeRoot == document.documentElement.parentNode);
}

/**
 * Returns the root element of the node.  Found by traversing parentNodes until
 * a node with no parent is found.  This node is considered the root.
 * @param {?Node} node The node to find the root element for.
 * @return {?Node} The root node.
 */
function getNodeRoot(node) {
  while (node && node.parentNode) {
    node = node.parentNode;
  }
  return node;
}

/**
 * Returns the root element of the node, jumping up through shadow roots if
 * any are found.
 */
function getNodeRootThroughAnyShadows(node) {
  let root = getNodeRoot(node);
  while (SHADOW_DOM_ENABLED && root instanceof ShadowRoot) {
    root = getNodeRoot(root.host);
  }
  return root;
}

/**
 * Returns whether given value is an element.
 * @param {*} value The value to identify as object.
 * @return {boolean} True if value is a cacheable element.
 */
function isElement(value) {
  // As of crrev.com/1316933002, typeof() for some elements will return
  // 'function', not 'object'. So we need to check for both non-null objects, as
  // well Elements that also happen to be callable functions (e.g. <embed> and
  // <object> elements). Note that we can not use |value instanceof Object| here
  // since this does not work with frames/iframes, for example
  // frames[0].document.body instanceof Object == false even though
  // typeof(frames[0].document.body) == 'object'.
  return ((typeof(value) == 'object' && value != null) ||
            (typeof(value) == 'function' && value.nodeName &&
            value.nodeType == NodeType.ELEMENT)) &&
          (value.nodeType == NodeType.ELEMENT   ||
           value.nodeType == NodeType.DOCUMENT  ||
           (SHADOW_DOM_ENABLED && value instanceof ShadowRoot));
}

/**
 * Returns whether given value is a collection (iterable with
 * 'length' property).
 * @param {*} value The value to identify as a collection.
 * @return {boolean} True if value is an iterable collection.
 */
function isCollection(value) {
  const Symbol = window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol || window.Symbol;
  return (typeof value[Symbol.iterator] === 'function') &&
      ('length' in value) &&
      (typeof value.length === 'number');
}

/**
 * Deep-clones item, given object references in seen, using cloning algorithm
 * algo. Implements "clone an object" from W3C-spec (#dfn-clone-an-object).
 * @param {*} item Object or collection to deep clone.
 * @param {!Array<*>} seen Object references that have already been seen.
 * @param {function(*, Array<*>) : *} algo Cloning algorithm to use to
 *     deep clone properties of item.
 * @param {!Array<*>} nodes List of serialized nodes
 * @return {*} Clone of item with status of cloning.
 */
function cloneWithAlgorithm(item, seen, algo, nodes) {
  let tmp = null;
  function maybeCopyProperty(prop) {
    let sourceValue = null;
    try {
      sourceValue = item[prop];
    } catch(e) {
      throw newError('error reading property', StatusCode.JAVA_SCRIPT_ERROR);
    }
    return algo(sourceValue, seen, nodes);
  }

  if (isCollection(item)) {
    const Array = window.cdc_adoQpoasnfa76pfcZLmcfl_Array || window.Array;
    tmp = new Array(item.length);
    for (let i = 0; i < item.length; ++i)
      tmp[i] = maybeCopyProperty(i);
  } else {
    tmp = {};
    for (let prop in item)
      tmp[prop] = maybeCopyProperty(prop);
  }
  return tmp;
}

/**
 * Wrapper to cloneWithAlgorithm, with circular reference detection logic.
 * @param {*} item Object or collection to deep clone.
 * @param {!Array<*>} seen Object references that have already been seen.
 * @param {function(*, Array<*>) : *} algo Cloning algorithm to use to
 *     deep clone properties of item.
 * @param {!Array<*>} nodes List of serialized nodes
 * @return {*} Clone of item with status of cloning.
 */
function cloneWithCircularCheck(item, seen, algo, nodes) {
  if (seen.includes(item))
    throw newError('circular reference', StatusCode.JAVA_SCRIPT_ERROR);
  seen.push(item);
  const result = cloneWithAlgorithm(item, seen, algo, nodes);
  seen.pop();
  return result;
}

/*
 * Prohibits call of object.prototype.toJSoN()
 */
function serializationGuard(object) {
  const handler = {
    get(target, name) {
      const value = target[name]
      if (typeof value !== 'function')
        return value;
      // Objects that have own toJSON are never guarded with a proxy.
      // All other functions are replaced with {} in preprocessResult.
      // The only remaining case when a client tries to access a method is a
      // call to non-own toJSON by JSON.stringify.
      // In this case this method needs to be concealed.
      return undefined;
    }
  }
  const Proxy = window.cdc_adoQpoasnfa76pfcZLmcfl_Proxy || window.Proxy;
  return new Proxy(object, handler);
}



/**
 * Returns deep clone of given value, replacing element references with a
 * serialized string representing that element.
 * @param {*} item Object or collection to deep clone.
 * @param {!Array<*>} seen Object references that have already been seen.
 * @param {!Array<*>} nodes List of serialized nodes
 * @return {*} Clone of item with status of cloning.
 */
function preprocessResult(item, seen, nodes) {
  if (item === undefined || item === null)
    return null;
  if (typeof item === 'boolean' ||
      typeof item === 'number' ||
      typeof item === 'string')
    return item;
  // We never descend to own property toJSON.
  // Any other function must be serialized as an object.
  if (typeof item === 'function')
    return {};
  if (isElement(item)) {
    if (!isNodeReachable(item)) {
      if (item instanceof ShadowRoot)
        throw newError('shadow root is detached from the current frame',
            StatusCode.DETACHED_SHADOW_ROOT);
      throw newError('stale element not found in the current frame',
                     StatusCode.STALE_ELEMENT_REFERENCE);
    }
    const ret = {};
    let key = ELEMENT_KEY;
    if (item instanceof ShadowRoot) {
      if (!item.nodeType ||
          item.nodeType !== item.DOCUMENT_FRAGMENT_NODE ||
          !item.host) {
        throw newError('no such shadow root', StatusCode.NO_SUCH_SHADOW_ROOT);
      }
      key = SHADOW_ROOT_KEY;
    }
    ret[key] = nodes.length;
    nodes.push(item);
    return serializationGuard(ret);
  }

  // TODO(crbug.com/40229283): Implement WindowProxy serialization.

  if (Object.hasOwn(item, 'toJSON') && typeof item.toJSON === 'function') {
      // Not guarded because we want item.toJSON to be invoked by
      // JSON.stringify.
      return item;
  }

  // Deep cloning of Array and Objects.
  return serializationGuard(
      cloneWithCircularCheck(item, seen, preprocessResult, nodes));
}

/**
 * Returns deserialized deep clone of given value, replacing serialized string
 * references to elements with a element reference, if found.
 * @param {*} item Object or collection to deep clone.
 * @param {!Array<*>} seen Object references that have already been seen.
 * @param {!Array<*>} nodes List of referred nodes
 * @return {*} Clone of item with status of cloning.
 */
function resolveReferencesRecursive(item, seen, nodes) {
  if (item === undefined ||
      item === null ||
      typeof item === 'boolean' ||
      typeof item === 'number' ||
      typeof item === 'string' ||
      typeof item === 'function')
    return item;
  if (item.hasOwnProperty(ELEMENT_KEY) ||
      item.hasOwnProperty(SHADOW_ROOT_KEY)) {
    let idx = item[ELEMENT_KEY];
    if (item.hasOwnProperty(SHADOW_ROOT_KEY))
      idx = item[SHADOW_ROOT_KEY];
    if (idx < 0 || idx >= nodes.length) {
      throw newError('unable to resove node reference. '
          + 'Node index is out of range.', StatusCode.JAVA_SCRIPT_ERROR);
    }
    return nodes[idx];
  }
  if (isCollection(item) || typeof item === 'object')
    return cloneWithAlgorithm(item, seen, resolveReferencesRecursive, nodes);
  throw newError('unhandled object', StatusCode.JAVA_SCRIPT_ERROR);
}

/**
 * Returns deserialized deep clone of given value, replacing serialized string
 * references to elements with a element reference, if found.
 * @param {*} item Object or collection to deep clone.
 * @param {!Array<*>} nodes List of referred nodes
 * @return {*} Clone of item with status of cloning.
 */
function resolveReferences(args, nodes) {
  for (let idx = 0; idx < nodes.length; ++idx) {
    if (!isNodeReachable(nodes[idx])) {
      if (nodes[idx] instanceof ShadowRoot)
        throw newError('shadow root is detached from the current frame',
            StatusCode.DETACHED_SHADOW_ROOT);
      throw newError('stale element not found in the current frame',
                     StatusCode.STALE_ELEMENT_REFERENCE);
    }
  }
  return resolveReferencesRecursive(args, [], nodes);
}

/**
 * Calls a given function and returns its value.
 *
 * The inputs to and outputs of the function will be unwrapped and wrapped
 * respectively, unless otherwise specified. This wrapping involves converting
 * between cached object reference IDs and actual JS objects.
 *
 * @param {function(...[*]) : *} func The function to invoke.
 * @param {!Array<*>} args The array of arguments to supply to the function,
 *     which will be unwrapped before invoking the function.
 * @param {boolean} w3c Whether to return a W3C compliant element reference.
 * @param {!Array<*>} Nodes referred in the arguments.
 * @return {*} An object containing a status and value property, where status
 *     is a WebDriver status code and value is the wrapped value. If an
 *     unwrapped return was specified, this will be the function's pure return
 *     value.
 */
function callFunction(func, args, w3c, nodes) {
  if (w3c) {
    w3cEnabled = true;
    ELEMENT_KEY = W3C_ELEMENT_KEY;

  }

  function buildError(error) {
    const errorResponse = serializationGuard({
      status: error.code || StatusCode.JAVA_SCRIPT_ERROR,
      value: error.message || error
    });
    const JSON = window.cdc_adoQpoasnfa76pfcZLmcfl_JSON || window.JSON;
    return [JSON.stringify(errorResponse)];
  }

  function wrapErrorAsJavascriptError(error) {
    originalResponse = buildError(error);
    originalStatus = error.code || StatusCode.JAVA_SCRIPT_ERROR;
    if (originalStatus === StatusCode.JAVA_SCRIPT_ERROR) {
      return originalResponse;
    }
    return buildError({
      code: StatusCode.JAVA_SCRIPT_ERROR,
      message: originalResponse[0]});
  }

  const Promise = window.cdc_adoQpoasnfa76pfcZLmcfl_Promise || window.Promise;
  let unwrappedArgs = null;
  try {
    unwrappedArgs = resolveReferences(args, nodes);
  } catch (error) {
    return Promise.resolve(buildError(error));
  }

  try {
    const tmp = func.apply(null, unwrappedArgs);
    return Promise.resolve(tmp).then((result) => {
      ret_nodes = [];
      const response = {
        status: 0,
        value: preprocessResult(result, [], ret_nodes)
      };
      const JSON = window.cdc_adoQpoasnfa76pfcZLmcfl_JSON || window.JSON;
      return [JSON.stringify(response), ...ret_nodes];
    }).catch(wrapErrorAsJavascriptError);
  } catch (error) {
    return Promise.resolve(wrapErrorAsJavascriptError(error));
  }
}