diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/server/actors/object/previewers.js | 1142 |
1 files changed, 1142 insertions, 0 deletions
diff --git a/devtools/server/actors/object/previewers.js b/devtools/server/actors/object/previewers.js new file mode 100644 index 0000000000..451858a826 --- /dev/null +++ b/devtools/server/actors/object/previewers.js @@ -0,0 +1,1142 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { DevToolsServer } = require("resource://devtools/server/devtools-server.js"); +const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); +loader.lazyRequireGetter( + this, + "ObjectUtils", + "resource://devtools/server/actors/object/utils.js" +); +loader.lazyRequireGetter( + this, + "PropertyIterators", + "resource://devtools/server/actors/object/property-iterator.js" +); + +// Number of items to preview in objects, arrays, maps, sets, lists, +// collections, etc. +const OBJECT_PREVIEW_MAX_ITEMS = 10; + +const ERROR_CLASSNAMES = new Set([ + "Error", + "EvalError", + "RangeError", + "ReferenceError", + "SyntaxError", + "TypeError", + "URIError", + "InternalError", + "AggregateError", + "CompileError", + "DebuggeeWouldRun", + "LinkError", + "RuntimeError", + "Exception", // This related to Components.Exception() +]); +const ARRAY_LIKE_CLASSNAMES = new Set([ + "DOMStringList", + "DOMTokenList", + "CSSRuleList", + "MediaList", + "StyleSheetList", + "NamedNodeMap", + "FileList", + "NodeList", +]); +const OBJECT_WITH_URL_CLASSNAMES = new Set([ + "CSSImportRule", + "CSSStyleSheet", + "Location", +]); + +/** + * Functions for adding information to ObjectActor grips for the purpose of + * having customized output. This object holds arrays mapped by + * Debugger.Object.prototype.class. + * + * In each array you can add functions that take three + * arguments: + * - the ObjectActor instance and its hooks to make a preview for, + * - the grip object being prepared for the client, + * - the raw JS object after calling Debugger.Object.unsafeDereference(). This + * argument is only provided if the object is safe for reading properties and + * executing methods. See DevToolsUtils.isSafeJSObject(). + * - the object class (result of objectActor.obj.class). This is passed so we don't have + * to access it on each previewer, which can add some overhead. + * + * Functions must return false if they cannot provide preview + * information for the debugger object, or true otherwise. + */ +const previewers = { + String: [ + function(objectActor, grip, rawObj) { + return wrappedPrimitivePreviewer( + "String", + String, + objectActor, + grip, + rawObj + ); + }, + ], + + Boolean: [ + function(objectActor, grip, rawObj) { + return wrappedPrimitivePreviewer( + "Boolean", + Boolean, + objectActor, + grip, + rawObj + ); + }, + ], + + Number: [ + function(objectActor, grip, rawObj) { + return wrappedPrimitivePreviewer( + "Number", + Number, + objectActor, + grip, + rawObj + ); + }, + ], + + Symbol: [ + function(objectActor, grip, rawObj) { + return wrappedPrimitivePreviewer( + "Symbol", + Symbol, + objectActor, + grip, + rawObj + ); + }, + ], + + Function: [ + function({ obj, hooks }, grip) { + if (obj.name) { + grip.name = obj.name; + } + + if (obj.displayName) { + grip.displayName = obj.displayName.substr(0, 500); + } + + if (obj.parameterNames) { + grip.parameterNames = obj.parameterNames; + } + + // Check if the developer has added a de-facto standard displayName + // property for us to use. + let userDisplayName; + try { + userDisplayName = obj.getOwnPropertyDescriptor("displayName"); + } catch (e) { + // The above can throw "permission denied" errors when the debuggee + // does not subsume the function's compartment. + } + + if ( + userDisplayName && + typeof userDisplayName.value == "string" && + userDisplayName.value + ) { + grip.userDisplayName = hooks.createValueGrip(userDisplayName.value); + } + + grip.isAsync = obj.isAsyncFunction; + grip.isGenerator = obj.isGeneratorFunction; + + if (obj.script) { + // NOTE: Debugger.Script.prototype.startColumn is 1-based. + // Convert to 0-based, while keeping the wasm's column (1) as is. + // (bug 1863878) + const columnBase = obj.script.format === "wasm" ? 0 : 1; + grip.location = { + url: obj.script.url, + line: obj.script.startLine, + column: obj.script.startColumn - columnBase, + }; + } + + return true; + }, + ], + + RegExp: [ + function({ obj, hooks }, grip) { + const str = DevToolsUtils.callPropertyOnObject(obj, "toString"); + if (typeof str != "string") { + return false; + } + + grip.displayString = hooks.createValueGrip(str); + return true; + }, + ], + + Date: [ + function({ obj, hooks }, grip) { + const time = DevToolsUtils.callPropertyOnObject(obj, "getTime"); + if (typeof time != "number") { + return false; + } + + grip.preview = { + timestamp: hooks.createValueGrip(time), + }; + return true; + }, + ], + + Array: [ + function({ obj, hooks }, grip) { + const length = ObjectUtils.getArrayLength(obj); + + grip.preview = { + kind: "ArrayLike", + length: length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + const raw = obj.unsafeDereference(); + const items = (grip.preview.items = []); + + for (let i = 0; i < length; ++i) { + if (raw && !isWorker) { + // Array Xrays filter out various possibly-unsafe properties (like + // functions, and claim that the value is undefined instead. This + // is generally the right thing for privileged code accessing untrusted + // objects, but quite confusing for Object previews. So we manually + // override this protection by waiving Xrays on the array, and re-applying + // Xrays on any indexed value props that we pull off of it. + const desc = Object.getOwnPropertyDescriptor(Cu.waiveXrays(raw), i); + if (desc && !desc.get && !desc.set) { + let value = Cu.unwaiveXrays(desc.value); + value = ObjectUtils.makeDebuggeeValueIfNeeded(obj, value); + items.push(hooks.createValueGrip(value)); + } else if (!desc) { + items.push(null); + } else { + const item = {}; + if (desc.get) { + let getter = Cu.unwaiveXrays(desc.get); + getter = ObjectUtils.makeDebuggeeValueIfNeeded(obj, getter); + item.get = hooks.createValueGrip(getter); + } + if (desc.set) { + let setter = Cu.unwaiveXrays(desc.set); + setter = ObjectUtils.makeDebuggeeValueIfNeeded(obj, setter); + item.set = hooks.createValueGrip(setter); + } + items.push(item); + } + } else if (raw && !obj.getOwnPropertyDescriptor(i)) { + items.push(null); + } else { + // Workers do not have access to Cu. + const value = DevToolsUtils.getProperty(obj, i); + items.push(hooks.createValueGrip(value)); + } + + if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + Set: [ + function(objectActor, grip) { + const size = DevToolsUtils.getProperty(objectActor.obj, "size"); + if (typeof size != "number") { + return false; + } + + grip.preview = { + kind: "ArrayLike", + length: size, + }; + + // Avoid recursive object grips. + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const items = (grip.preview.items = []); + for (const item of PropertyIterators.enumSetEntries(objectActor)) { + items.push(item); + if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + WeakSet: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumWeakSetEntries(objectActor); + + grip.preview = { + kind: "ArrayLike", + length: enumEntries.size, + }; + + // Avoid recursive object grips. + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const items = (grip.preview.items = []); + for (const item of enumEntries) { + items.push(item); + if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + Map: [ + function(objectActor, grip) { + const size = DevToolsUtils.getProperty(objectActor.obj, "size"); + if (typeof size != "number") { + return false; + } + + grip.preview = { + kind: "MapLike", + size: size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of PropertyIterators.enumMapEntries(objectActor)) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + WeakMap: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumWeakMapEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + URLSearchParams: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumURLSearchParamsEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + FormData: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumFormDataEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + Headers: [ + function(objectActor, grip) { + // Bug 1863776: Headers can't be yet previewed from workers + if (isWorker) { + return false; + } + const enumEntries = PropertyIterators.enumHeadersEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + + HighlightRegistry: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumHighlightRegistryEntries(objectActor); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + MIDIInputMap: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumMidiInputMapEntries( + objectActor + ); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + MIDIOutputMap: [ + function(objectActor, grip) { + const enumEntries = PropertyIterators.enumMidiOutputMapEntries( + objectActor + ); + + grip.preview = { + kind: "MapLike", + size: enumEntries.size, + }; + + if (objectActor.hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const entry of enumEntries) { + entries.push(entry); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + DOMStringMap: [ + function({ obj, hooks }, grip, rawObj) { + if (!rawObj) { + return false; + } + + const keys = obj.getOwnPropertyNames(); + grip.preview = { + kind: "MapLike", + size: keys.length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + const entries = (grip.preview.entries = []); + for (const key of keys) { + const value = ObjectUtils.makeDebuggeeValueIfNeeded(obj, rawObj[key]); + entries.push([key, hooks.createValueGrip(value)]); + if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + return true; + }, + ], + + Promise: [ + function({ obj, hooks }, grip, rawObj) { + const { state, value, reason } = ObjectUtils.getPromiseState(obj); + const ownProperties = Object.create(null); + ownProperties["<state>"] = { value: state }; + let ownPropertiesLength = 1; + + // Only expose <value> or <reason> in top-level promises, to avoid recursion. + // <state> is not problematic because it's a string. + if (hooks.getGripDepth() === 1) { + if (state == "fulfilled") { + ownProperties["<value>"] = { value: hooks.createValueGrip(value) }; + ++ownPropertiesLength; + } else if (state == "rejected") { + ownProperties["<reason>"] = { value: hooks.createValueGrip(reason) }; + ++ownPropertiesLength; + } + } + + grip.preview = { + kind: "Object", + ownProperties, + ownPropertiesLength, + }; + + return true; + }, + ], + + Proxy: [ + function({ obj, hooks }, grip, rawObj) { + // Only preview top-level proxies, avoiding recursion. Otherwise, since both the + // target and handler can also be proxies, we could get an exponential behavior. + if (hooks.getGripDepth() > 1) { + return true; + } + + // The `isProxy` getter of the debuggee object only detects proxies without + // security wrappers. If false, the target and handler are not available. + const hasTargetAndHandler = obj.isProxy; + + grip.preview = { + kind: "Object", + ownProperties: Object.create(null), + ownPropertiesLength: 2 * hasTargetAndHandler, + }; + + if (hasTargetAndHandler) { + Object.assign(grip.preview.ownProperties, { + "<target>": { value: hooks.createValueGrip(obj.proxyTarget) }, + "<handler>": { value: hooks.createValueGrip(obj.proxyHandler) }, + }); + } + + return true; + }, + ], +}; + +/** + * Generic previewer for classes wrapping primitives, like String, + * Number and Boolean. + * + * @param string className + * Class name to expect. + * @param object classObj + * The class to expect, eg. String. The valueOf() method of the class is + * invoked on the given object. + * @param ObjectActor objectActor + * The object actor + * @param Object grip + * The result grip to fill in + * @return Booolean true if the object was handled, false otherwise + */ +function wrappedPrimitivePreviewer( + className, + classObj, + objectActor, + grip, + rawObj +) { + let v = null; + try { + v = classObj.prototype.valueOf.call(rawObj); + } catch (ex) { + // valueOf() can throw if the raw JS object is "misbehaved". + return false; + } + + if (v === null) { + return false; + } + + const { obj, hooks } = objectActor; + + const canHandle = GenericObject(objectActor, grip, rawObj, className); + if (!canHandle) { + return false; + } + + grip.preview.wrappedValue = hooks.createValueGrip( + ObjectUtils.makeDebuggeeValueIfNeeded(obj, v) + ); + return true; +} + +/** + * @param {ObjectActor} objectActor + * @param {Object} grip: The grip built by the objectActor, for which we need to populate + * the `preview` property. + * @param {*} rawObj: The native js object + * @param {String} className: objectActor.obj.class + * @returns + */ +function GenericObject(objectActor, grip, rawObj, className) { + const { obj, hooks } = objectActor; + if (grip.preview || grip.displayString || hooks.getGripDepth() > 1) { + return false; + } + + const preview = (grip.preview = { + kind: "Object", + ownProperties: Object.create(null), + }); + + const names = ObjectUtils.getPropNamesFromObject(obj, rawObj); + preview.ownPropertiesLength = names.length; + + let length, + i = 0; + let specialStringBehavior = className === "String"; + if (specialStringBehavior) { + length = DevToolsUtils.getProperty(obj, "length"); + if (typeof length != "number") { + specialStringBehavior = false; + } + } + + for (const name of names) { + if (specialStringBehavior && /^[0-9]+$/.test(name)) { + const num = parseInt(name, 10); + if (num.toString() === name && num >= 0 && num < length) { + continue; + } + } + + const desc = objectActor._propertyDescriptor(name, true); + if (!desc) { + continue; + } + + preview.ownProperties[name] = desc; + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + + if (i === OBJECT_PREVIEW_MAX_ITEMS) { + return true; + } + + const privatePropertiesSymbols = ObjectUtils.getSafePrivatePropertiesSymbols( + obj + ); + if (privatePropertiesSymbols.length > 0) { + preview.privatePropertiesLength = privatePropertiesSymbols.length; + preview.privateProperties = []; + + // Retrieve private properties, which are represented as non-enumerable Symbols + for (const privateProperty of privatePropertiesSymbols) { + if ( + !privateProperty.description || + !privateProperty.description.startsWith("#") + ) { + continue; + } + const descriptor = objectActor._propertyDescriptor(privateProperty); + if (!descriptor) { + continue; + } + + preview.privateProperties.push( + Object.assign( + { + descriptor, + }, + hooks.createValueGrip(privateProperty) + ) + ); + + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + + if (i === OBJECT_PREVIEW_MAX_ITEMS) { + return true; + } + + const symbols = ObjectUtils.getSafeOwnPropertySymbols(obj); + if (symbols.length > 0) { + preview.ownSymbolsLength = symbols.length; + preview.ownSymbols = []; + + for (const symbol of symbols) { + const descriptor = objectActor._propertyDescriptor(symbol, true); + if (!descriptor) { + continue; + } + + preview.ownSymbols.push( + Object.assign( + { + descriptor, + }, + hooks.createValueGrip(symbol) + ) + ); + + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + + if (i === OBJECT_PREVIEW_MAX_ITEMS) { + return true; + } + + const safeGetterValues = objectActor._findSafeGetterValues( + Object.keys(preview.ownProperties), + OBJECT_PREVIEW_MAX_ITEMS - i + ); + if (Object.keys(safeGetterValues).length) { + preview.safeGetterValues = safeGetterValues; + } + + return true; +} + +// Preview functions that do not rely on the object class. +previewers.Object = [ + function TypedArray({ obj, hooks }, grip) { + if (!ObjectUtils.isTypedArray(obj)) { + return false; + } + + grip.preview = { + kind: "ArrayLike", + length: ObjectUtils.getArrayLength(obj), + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + const previewLength = Math.min( + OBJECT_PREVIEW_MAX_ITEMS, + grip.preview.length + ); + grip.preview.items = []; + for (let i = 0; i < previewLength; i++) { + const desc = obj.getOwnPropertyDescriptor(i); + if (!desc) { + break; + } + grip.preview.items.push(desc.value); + } + + return true; + }, + + function Error(objectActor, grip, rawObj, className) { + if (!ERROR_CLASSNAMES.has(className)) { + return false; + } + + const { hooks, obj } = objectActor; + + // The name and/or message could be getters, and even if it's unsafe, we do want + // to show it to the user (See Bug 1710694). + const name = DevToolsUtils.getProperty(obj, "name", true); + const msg = DevToolsUtils.getProperty(obj, "message", true); + const stack = DevToolsUtils.getProperty(obj, "stack"); + const fileName = DevToolsUtils.getProperty(obj, "fileName"); + const lineNumber = DevToolsUtils.getProperty(obj, "lineNumber"); + const columnNumber = DevToolsUtils.getProperty(obj, "columnNumber"); + + grip.preview = { + kind: "Error", + name: hooks.createValueGrip(name), + message: hooks.createValueGrip(msg), + stack: hooks.createValueGrip(stack), + fileName: hooks.createValueGrip(fileName), + lineNumber: hooks.createValueGrip(lineNumber), + columnNumber: hooks.createValueGrip(columnNumber), + }; + + const errorHasCause = obj.getOwnPropertyNames().includes("cause"); + if (errorHasCause) { + grip.preview.cause = hooks.createValueGrip( + DevToolsUtils.getProperty(obj, "cause", true) + ); + } + + return true; + }, + + function CSSMediaRule(objectActor, grip, rawObj, className) { + if (!rawObj || className != "CSSMediaRule" || isWorker) { + return false; + } + const { hooks } = objectActor; + grip.preview = { + kind: "ObjectWithText", + text: hooks.createValueGrip(rawObj.conditionText), + }; + return true; + }, + + function CSSStyleRule(objectActor, grip, rawObj, className) { + if (!rawObj || className != "CSSStyleRule" || isWorker) { + return false; + } + const { hooks } = objectActor; + grip.preview = { + kind: "ObjectWithText", + text: hooks.createValueGrip(rawObj.selectorText), + }; + return true; + }, + + function ObjectWithURL(objectActor, grip, rawObj, className) { + if (isWorker || !rawObj) { + return false; + } + + const isWindow = Window.isInstance(rawObj); + if (!OBJECT_WITH_URL_CLASSNAMES.has(className) && !isWindow) { + return false; + } + + const { hooks } = objectActor; + + let url; + if (isWindow && rawObj.location) { + try { + url = rawObj.location.href; + } catch(e) { + // This can happen when we have a cross-process window. + // In such case, let's retrieve the url from the iframe. + // For window.top from a remote iframe, there's no way we can't retrieve the URL, + // so return a label that help user know what's going on. + url = rawObj.browsingContext?.embedderElement?.src || "Restricted"; + } + } else if (rawObj.href) { + url = rawObj.href; + } else { + return false; + } + + grip.preview = { + kind: "ObjectWithURL", + url: hooks.createValueGrip(url), + }; + + return true; + }, + + function ArrayLike(objectActor, grip, rawObj, className) { + if ( + !rawObj || + !ARRAY_LIKE_CLASSNAMES.has(className) || + typeof rawObj.length != "number" || + isWorker + ) { + return false; + } + + const { obj, hooks } = objectActor; + grip.preview = { + kind: "ArrayLike", + length: rawObj.length, + }; + + if (hooks.getGripDepth() > 1) { + return true; + } + + const items = (grip.preview.items = []); + + for ( + let i = 0; + i < rawObj.length && items.length < OBJECT_PREVIEW_MAX_ITEMS; + i++ + ) { + const value = ObjectUtils.makeDebuggeeValueIfNeeded(obj, rawObj[i]); + items.push(hooks.createValueGrip(value)); + } + + return true; + }, + + function CSSStyleDeclaration(objectActor, grip, rawObj, className) { + if ( + !rawObj || + (className != "CSSStyleDeclaration" && className != "CSS2Properties") || + isWorker + ) { + return false; + } + + const { hooks } = objectActor; + grip.preview = { + kind: "MapLike", + size: rawObj.length, + }; + + const entries = (grip.preview.entries = []); + + for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS && i < rawObj.length; i++) { + const prop = rawObj[i]; + const value = rawObj.getPropertyValue(prop); + entries.push([prop, hooks.createValueGrip(value)]); + } + + return true; + }, + + function DOMNode(objectActor, grip, rawObj, className) { + if ( + className == "Object" || + !rawObj || + !Node.isInstance(rawObj) || + isWorker + ) { + return false; + } + + const { obj, hooks } = objectActor; + + const preview = (grip.preview = { + kind: "DOMNode", + nodeType: rawObj.nodeType, + nodeName: rawObj.nodeName, + isConnected: rawObj.isConnected === true, + }); + + if (rawObj.nodeType == rawObj.DOCUMENT_NODE && rawObj.location) { + preview.location = hooks.createValueGrip(rawObj.location.href); + } else if (obj.class == "DocumentFragment") { + preview.childNodesLength = rawObj.childNodes.length; + + if (hooks.getGripDepth() < 2) { + preview.childNodes = []; + for (const node of rawObj.childNodes) { + const actor = hooks.createValueGrip(obj.makeDebuggeeValue(node)); + preview.childNodes.push(actor); + if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + } else if (Element.isInstance(rawObj)) { + // For HTML elements (in an HTML document, at least), the nodeName is an + // uppercased version of the actual element name. Check for HTML + // elements, that is elements in the HTML namespace, and lowercase the + // nodeName in that case. + if (rawObj.namespaceURI == "http://www.w3.org/1999/xhtml") { + preview.nodeName = preview.nodeName.toLowerCase(); + } + + // Add preview for DOM element attributes. + preview.attributes = {}; + preview.attributesLength = rawObj.attributes.length; + for (const attr of rawObj.attributes) { + preview.attributes[attr.nodeName] = hooks.createValueGrip(attr.value); + } + } else if (obj.class == "Attr") { + preview.value = hooks.createValueGrip(rawObj.value); + } else if ( + obj.class == "Text" || + obj.class == "CDATASection" || + obj.class == "Comment" + ) { + preview.textContent = hooks.createValueGrip(rawObj.textContent); + } + + return true; + }, + + function DOMEvent(objectActor, grip, rawObj) { + if (!rawObj || !Event.isInstance(rawObj) || isWorker) { + return false; + } + + const { obj, hooks } = objectActor; + const preview = (grip.preview = { + kind: "DOMEvent", + type: rawObj.type, + properties: Object.create(null), + }); + + if (hooks.getGripDepth() < 2) { + const target = obj.makeDebuggeeValue(rawObj.target); + preview.target = hooks.createValueGrip(target); + } + + if (obj.class == "KeyboardEvent") { + preview.eventKind = "key"; + preview.modifiers = ObjectUtils.getModifiersForEvent(rawObj); + } + + const props = ObjectUtils.getPropsForEvent(obj.class); + + // Add event-specific properties. + for (const prop of props) { + let value = rawObj[prop]; + if (ObjectUtils.isObjectOrFunction(value)) { + // Skip properties pointing to objects. + if (hooks.getGripDepth() > 1) { + continue; + } + value = obj.makeDebuggeeValue(value); + } + preview.properties[prop] = hooks.createValueGrip(value); + } + + // Add any properties we find on the event object. + if (!props.length) { + let i = 0; + for (const prop in rawObj) { + let value = rawObj[prop]; + if ( + prop == "target" || + prop == "type" || + value === null || + typeof value == "function" + ) { + continue; + } + if (value && typeof value == "object") { + if (hooks.getGripDepth() > 1) { + continue; + } + value = obj.makeDebuggeeValue(value); + } + preview.properties[prop] = hooks.createValueGrip(value); + if (++i == OBJECT_PREVIEW_MAX_ITEMS) { + break; + } + } + } + + return true; + }, + + function DOMException(objectActor, grip, rawObj, className) { + if (!rawObj || className !== "DOMException" || isWorker) { + return false; + } + + const { hooks } = objectActor; + grip.preview = { + kind: "DOMException", + name: hooks.createValueGrip(rawObj.name), + message: hooks.createValueGrip(rawObj.message), + code: hooks.createValueGrip(rawObj.code), + result: hooks.createValueGrip(rawObj.result), + filename: hooks.createValueGrip(rawObj.filename), + lineNumber: hooks.createValueGrip(rawObj.lineNumber), + columnNumber: hooks.createValueGrip(rawObj.columnNumber), + stack: hooks.createValueGrip(rawObj.stack), + }; + + return true; + }, + + function Object(objectActor, grip, rawObj, className) { + return GenericObject(objectActor, grip, rawObj, className); + }, +]; + +module.exports = previewers; |