| // 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('detached shadow root', StatusCode.DETACHED_SHADOW_ROOT); |
| throw newError('stale element not found', |
| 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/1337415): 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 resolveReferences(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)) { |
| const idx = item[ELEMENT_KEY]; |
| if (idx < 0 || idx >= nodes.length) { |
| throw newError('element is not attached to the page document', |
| StatusCode.STALE_ELEMENT_REFERENCE); |
| } |
| return nodes[idx]; |
| } |
| if (item.hasOwnProperty(SHADOW_ROOT_KEY)) { |
| const idx = item[SHADOW_ROOT_KEY]; |
| if (idx < 0 || idx >= nodes.length) { |
| throw newError('detached shadow root', StatusCode.DETACHED_SHADOW_ROOT); |
| } |
| return nodes[idx]; |
| } |
| if (isCollection(item) || typeof item === 'object') |
| return cloneWithAlgorithm(item, seen, resolveReferences, nodes); |
| throw newError('unhandled object', StatusCode.JAVA_SCRIPT_ERROR); |
| } |
| |
| /** |
| * 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)]; |
| } |
| |
| const Promise = window.cdc_adoQpoasnfa76pfcZLmcfl_Promise || window.Promise; |
| try { |
| for (let idx = 0; idx < nodes.length; ++idx) { |
| if (!isNodeReachable(nodes[idx])) { |
| if (nodes[idx] instanceof ShadowRoot) |
| throw newError('detached shadow root', |
| StatusCode.DETACHED_SHADOW_ROOT); |
| throw newError('stale element not found', |
| StatusCode.STALE_ELEMENT_REFERENCE); |
| } |
| } |
| const unwrappedArgs = resolveReferences(args, [], nodes); |
| 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(buildError); |
| } catch (error) { |
| return Promise.resolve(buildError(error)); |
| } |
| } |