blob: aa0e44d47685eabe8d07150d539b31a65221a4f6 [file] [log] [blame]
/*
* 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.subtype === "array") {
// COMPATIBILITY (iOS 8): Runtime.RemoteObject did not have size property,
// instead it was tacked onto the end of the description, like "Array[#]".
var match = payload.description.match(/\[(\d+)\]$/);
if (match) {
payload.size = parseInt(match[1]);
payload.description = payload.description.replace(/\[\d+\]$/, "");
}
}
if (payload.classPrototype)
payload.classPrototype = WI.RemoteObject.fromPayload(payload.classPrototype, target);
if (payload.preview) {
// COMPATIBILITY (iOS 8): Did not have type/subtype/description on
// Runtime.ObjectPreview. Copy them over from the RemoteObject.
if (!payload.preview.type) {
payload.preview.type = payload.type;
payload.preview.subtype = payload.subtype;
payload.preview.description = payload.description;
payload.preview.size = payload.size;
}
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)
{
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);
}
// 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;
}
// COMPATIBILITY (iOS 8): Runtime.getDisplayableProperties did not exist.
// Here we do our best to reimplement it by getting all properties and reducing them down.
if (!this._target.hasCommand("Runtime.getDisplayableProperties")) {
this._getProperties(options, (error, allProperties) => {
var ownOrGetterPropertiesList = [];
if (allProperties) {
for (var property of allProperties) {
if (property.isOwn || property.name === "__proto__") {
// Own property or getter property in prototype chain.
ownOrGetterPropertiesList.push(property);
} else if (property.value && property.name !== property.name.toUpperCase()) {
var type = property.value.type;
if (type && type !== "function" && property.name !== "constructor") {
// Possible native binding getter property converted to a value. Also, no CONSTANT name style and not "constructor".
// There is no way of knowing if this is native or not, so just go with it.
ownOrGetterPropertiesList.push(property);
}
}
}
}
this._getPropertyDescriptorsResolver(callback, error, ownOrGetterPropertiesList);
});
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)
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"
};