/* * Copyright (C) 2009 Google Inc. All rights reserved. * Copyright (C) 2015 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ WI.RemoteObject = class RemoteObject { constructor(target, objectId, type, subtype, value, description, size, classPrototype, className, preview) { console.assert(type); console.assert(!preview || preview instanceof WI.ObjectPreview); console.assert(!target || target instanceof WI.Target); this._target = target || WI.mainTarget; this._type = type; this._subtype = subtype; if (objectId) { // Object, Function, or Symbol. console.assert(!subtype || typeof subtype === "string"); console.assert(!description || typeof description === "string"); console.assert(!value); this._objectId = objectId; this._description = description || ""; this._hasChildren = type !== "symbol"; this._size = size; this._classPrototype = classPrototype; this._preview = preview; if (subtype === "class") { this._functionDescription = this._description; this._description = "class " + className; } } else { // Primitive, BigInt, or null. console.assert(type !== "object" || value === null); console.assert(!preview); this._description = description || (value + ""); this._hasChildren = false; this._value = value; if (type === "bigint") { console.assert(value === undefined); console.assert(description.endsWith("n")); if (window.BigInt) this._value = BigInt(description.substring(0, description.length - 1)); else this._value = `${description} [BigInt Not Enabled in Web Inspector]`; } } } // Static static createFakeRemoteObject() { return new WI.RemoteObject(undefined, WI.RemoteObject.FakeRemoteObjectId, "object"); } static fromPrimitiveValue(value) { return new WI.RemoteObject(undefined, undefined, typeof value, undefined, value, undefined, undefined, undefined, undefined); } static createBigIntFromDescriptionString(description) { console.assert(description.endsWith("n")); return new WI.RemoteObject(undefined, undefined, "bigint", undefined, undefined, description, undefined, undefined, undefined); } static fromPayload(payload, target) { console.assert(typeof payload === "object", "Remote object payload should only be an object"); if (payload.classPrototype) payload.classPrototype = WI.RemoteObject.fromPayload(payload.classPrototype, target); if (payload.preview) payload.preview = WI.ObjectPreview.fromPayload(payload.preview); return new WI.RemoteObject(target, payload.objectId, payload.type, payload.subtype, payload.value, payload.description, payload.size, payload.classPrototype, payload.className, payload.preview); } static createCallArgument(valueOrObject) { if (valueOrObject instanceof WI.RemoteObject) { if (valueOrObject.objectId) return {objectId: valueOrObject.objectId}; return {value: valueOrObject.value}; } return {value: valueOrObject}; } static resolveNode(node, objectGroup) { console.assert(node instanceof WI.DOMNode, node); if (node.destroyed) return Promise.reject("ERROR: node is destroyed"); let target = WI.assumingMainTarget(); return target.DOMAgent.resolveNode(node.id, objectGroup) .then(({object}) => WI.RemoteObject.fromPayload(object, WI.mainTarget)); } static resolveWebSocket(webSocketResource, objectGroup, callback) { console.assert(typeof callback === "function"); let target = WI.assumingMainTarget(); target.NetworkAgent.resolveWebSocket(webSocketResource.requestIdentifier, objectGroup, (error, object) => { if (error || !object) callback(null); else callback(WI.RemoteObject.fromPayload(object, webSocketResource.target)); }); } static resolveCanvasContext(canvas, objectGroup, callback) { console.assert(typeof callback === "function"); function wrapCallback(error, object) { if (error || !object) callback(null); else callback(WI.RemoteObject.fromPayload(object, WI.mainTarget)); } let target = WI.assumingMainTarget(); // COMPATIBILITY (iOS 13): Canvas.resolveCanvasContext was renamed to Canvas.resolveContext. if (!target.hasCommand("Canvas.resolveContext")) { target.CanvasAgent.resolveCanvasContext(canvas.identifier, objectGroup, wrapCallback); return; } target.CanvasAgent.resolveContext(canvas.identifier, objectGroup, wrapCallback); } static resolveAnimation(animation, objectGroup, callback) { console.assert(typeof callback === "function"); function wrapCallback(error, object) { if (error || !object) callback(null); else callback(WI.RemoteObject.fromPayload(object, WI.mainTarget)); } let target = WI.assumingMainTarget(); // COMPATIBILITY (iOS 13.1): Animation.resolveAnimation did not exist yet. console.assert(target.hasCommand("Animation.resolveAnimation")); target.AnimationAgent.resolveAnimation(animation.animationId, objectGroup, wrapCallback); } // Public get target() { return this._target; } get objectId() { return this._objectId; } get type() { return this._type; } get subtype() { return this._subtype; } get description() { return this._description; } get functionDescription() { console.assert(this.type === "function"); return this._functionDescription || this._description; } get hasChildren() { return this._hasChildren; } get value() { return this._value; } get size() { return this._size || 0; } get classPrototype() { return this._classPrototype; } get preview() { return this._preview; } hasSize() { return this.isArray() || this.isCollectionType(); } hasValue() { return "_value" in this; } canLoadPreview() { if (this._failedToLoadPreview) return false; if (this._type !== "object") return false; if (!this._objectId || this._isSymbol() || this._isFakeObject()) return false; return true; } updatePreview(callback) { if (!this.canLoadPreview()) { callback(null); return; } if (!this._target.hasCommand("Runtime.getPreview")) { this._failedToLoadPreview = true; callback(null); return; } this._target.RuntimeAgent.getPreview(this._objectId, (error, payload) => { if (error) { this._failedToLoadPreview = true; callback(null); return; } this._preview = WI.ObjectPreview.fromPayload(payload); callback(this._preview); }); } getPropertyDescriptors(callback, options = {}) { if (!this._objectId || this._isSymbol() || this._isFakeObject()) { callback([]); return; } this._getProperties(this._getPropertyDescriptorsResolver.bind(this, callback), options); } getDisplayablePropertyDescriptors(callback, options = {}) { if (!this._objectId || this._isSymbol() || this._isFakeObject()) { callback([]); return; } this._getDisplayableProperties(this._getPropertyDescriptorsResolver.bind(this, callback), options); } setPropertyValue(name, value, callback) { if (!this._objectId || this._isSymbol() || this._isFakeObject()) { callback("Can't set a property of non-object."); return; } // FIXME: It doesn't look like setPropertyValue is used yet. This will need to be tested when it is again (editable ObjectTrees). this._target.RuntimeAgent.evaluate.invoke({expression: appendWebInspectorSourceURL(value), doNotPauseOnExceptionsAndMuteConsole: true}, evaluatedCallback.bind(this)); function evaluatedCallback(error, result, wasThrown) { if (error || wasThrown) { callback(error || result.description); return; } function setPropertyValue(propertyName, propertyValue) { this[propertyName] = propertyValue; } delete result.description; // Optimize on traffic. this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(setPropertyValue.toString()), [{value: name}, result], true, undefined, propertySetCallback.bind(this)); if (result._objectId) this._target.RuntimeAgent.releaseObject(result._objectId); } function propertySetCallback(error, result, wasThrown) { if (error || wasThrown) { callback(error || result.description); return; } callback(); } } isUndefined() { return this._type === "undefined"; } isNode() { return this._subtype === "node"; } isArray() { return this._subtype === "array"; } isClass() { return this._subtype === "class"; } isCollectionType() { return this._subtype === "map" || this._subtype === "set" || this._subtype === "weakmap" || this._subtype === "weakset"; } isWeakCollection() { return this._subtype === "weakmap" || this._subtype === "weakset"; } getCollectionEntries(callback, {fetchStart, fetchCount} = {}) { console.assert(this.isCollectionType()); console.assert(typeof fetchStart === "undefined" || (typeof fetchStart === "number" && fetchStart >= 0), fetchStart); console.assert(typeof fetchCount === "undefined" || (typeof fetchCount === "number" && fetchCount > 0), fetchCount); // WeakMaps and WeakSets are not ordered. We should never send a non-zero start. console.assert(!this.isWeakCollection() || typeof fetchStart === "undefined" || fetchStart === 0, fetchStart); let objectGroup = this.isWeakCollection() ? this._weakCollectionObjectGroup() : ""; // COMPATIBILITY (iOS 13): `startIndex` and `numberToFetch` were renamed to `fetchStart` and `fetchCount` (but kept in the same position). this._target.RuntimeAgent.getCollectionEntries(this._objectId, objectGroup, fetchStart, fetchCount, (error, entries) => { callback(entries.map((x) => WI.CollectionEntry.fromPayload(x, this._target))); }); } releaseWeakCollectionEntries() { console.assert(this.isWeakCollection()); this._target.RuntimeAgent.releaseObjectGroup(this._weakCollectionObjectGroup()); } pushNodeToFrontend(callback) { if (this._objectId && InspectorBackend.hasCommand("DOM.requestNode")) WI.domManager.pushNodeToFrontend(this._objectId, callback); else callback(0); } async fetchProperties(propertyNames, resultObject = {}) { let seenPropertyNames = new Set; let requestedValues = []; for (let propertyName of propertyNames) { // Check this here, otherwise things like '{}' would be valid Set keys. if (typeof propertyName !== "string" && typeof propertyName !== "number") throw new Error(`Tried to get property using key is not a string or number: ${propertyName}`); if (seenPropertyNames.has(propertyName)) continue; seenPropertyNames.add(propertyName); requestedValues.push(this.getProperty(propertyName)); } // Return primitive values directly, otherwise return a WI.RemoteObject instance. function maybeUnwrapValue(remoteObject) { return remoteObject.hasValue() ? remoteObject.value : remoteObject; } // Request property values one by one, since returning an array of property // values would then be subject to arbitrary object preview size limits. let fetchedKeys = Array.from(seenPropertyNames); let fetchedValues = await Promise.all(requestedValues); for (let i = 0; i < fetchedKeys.length; ++i) resultObject[fetchedKeys[i]] = maybeUnwrapValue(fetchedValues[i]); return resultObject; } getProperty(propertyName, callback = null) { function inspectedPage_object_getProperty(property) { if (typeof property !== "string" && typeof property !== "number") throw new Error(`Tried to get property using key is not a string or number: ${property}`); return this[property]; } if (callback && typeof callback === "function") this.callFunction(inspectedPage_object_getProperty, [propertyName], true, callback); else return this.callFunction(inspectedPage_object_getProperty, [propertyName], true); } callFunction(functionDeclaration, args, generatePreview, callback = null) { let translateResult = (result) => result ? WI.RemoteObject.fromPayload(result, this._target) : null; if (args) args = args.map(WI.RemoteObject.createCallArgument); if (callback && typeof callback === "function") { this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, undefined, !!generatePreview, (error, result, wasThrown) => { callback(error, translateResult(result), wasThrown); }); } else { // Protocol errors and results that were thrown should cause promise rejection with the same. return this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, undefined, !!generatePreview) .then(({result, wasThrown}) => { result = translateResult(result); if (result && wasThrown) return Promise.reject(result); return Promise.resolve(result); }); } } callFunctionJSON(functionDeclaration, args, callback) { function mycallback(error, result, wasThrown) { callback((error || wasThrown) ? null : result.value); } this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, true, mycallback); } invokeGetter(getterRemoteObject, callback) { console.assert(getterRemoteObject instanceof WI.RemoteObject); function backendInvokeGetter(getter) { return getter ? getter.call(this) : undefined; } this.callFunction(backendInvokeGetter, [getterRemoteObject], true, callback); } getOwnPropertyDescriptor(propertyName, callback) { function backendGetOwnPropertyDescriptor(propertyName) { return this[propertyName]; } function wrappedCallback(error, result, wasThrown) { if (error || wasThrown || !(result instanceof WI.RemoteObject)) { callback(null); return; } var fakeDescriptor = {name: propertyName, value: result, writable: true, configurable: true}; var fakePropertyDescriptor = new WI.PropertyDescriptor(fakeDescriptor, null, true, false, false, false); callback(fakePropertyDescriptor); } // FIXME: Implement a real RuntimeAgent.getOwnPropertyDescriptor? this.callFunction(backendGetOwnPropertyDescriptor, [propertyName], false, wrappedCallback.bind(this)); } release() { if (this._objectId && !this._isFakeObject()) this._target.RuntimeAgent.releaseObject(this._objectId); } arrayLength() { if (this._subtype !== "array") return 0; var matches = this._description.match(/\[([0-9]+)\]/); if (!matches) return 0; return parseInt(matches[1], 10); } asCallArgument() { return WI.RemoteObject.createCallArgument(this); } findFunctionSourceCodeLocation() { var result = new WI.WrappedPromise; if (!this._isFunction() || !this._objectId) { result.resolve(WI.RemoteObject.SourceCodeLocationPromise.MissingObjectId); return result.promise; } this._target.DebuggerAgent.getFunctionDetails(this._objectId, (error, response) => { if (error) { result.resolve(WI.RemoteObject.SourceCodeLocationPromise.NoSourceFound); return; } var location = response.location; var sourceCode = WI.debuggerManager.scriptForIdentifier(location.scriptId, this._target); if (!sourceCode || (!WI.settings.engineeringShowInternalScripts.value && isWebKitInternalScript(sourceCode.sourceURL))) { result.resolve(WI.RemoteObject.SourceCodeLocationPromise.NoSourceFound); return; } var sourceCodeLocation = sourceCode.createSourceCodeLocation(location.lineNumber, location.columnNumber || 0); result.resolve(sourceCodeLocation); }); return result.promise; } // Private _isFakeObject() { return this._objectId === WI.RemoteObject.FakeRemoteObjectId; } _isSymbol() { return this._type === "symbol"; } _isFunction() { return this._type === "function"; } _weakCollectionObjectGroup() { return JSON.stringify(this._objectId) + "-" + this._subtype; } _getProperties(callback, {ownProperties, fetchStart, fetchCount, generatePreview} = {}) { // COMPATIBILITY (iOS 13): `result` was renamed to `properties` (but kept in the same position). this._target.RuntimeAgent.getProperties.invoke({ objectId: this._objectId, ownProperties, fetchStart, fetchCount, generatePreview, }, callback); } _getDisplayableProperties(callback, {fetchStart, fetchCount, generatePreview} = {}) { console.assert(this._target.hasCommand("Runtime.getDisplayableProperties")); this._target.RuntimeAgent.getDisplayableProperties.invoke({ objectId: this._objectId, fetchStart, fetchCount, generatePreview, }, callback); } _getPropertyDescriptorsResolver(callback, error, properties, internalProperties) { if (error) { callback(null); return; } let descriptors = properties.map((payload) => WI.PropertyDescriptor.fromPayload(payload, false, this._target)); if (internalProperties) { for (let payload of internalProperties) descriptors.push(WI.PropertyDescriptor.fromPayload(payload, true, this._target)); } callback(descriptors); } }; WI.RemoteObject.FakeRemoteObjectId = "fake-remote-object"; WI.RemoteObject.SourceCodeLocationPromise = { NoSourceFound: "remote-object-source-code-location-promise-no-source-found", MissingObjectId: "remote-object-source-code-location-promise-missing-object-id" };