/* 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 { Actor } = require("resource://devtools/shared/protocol.js"); const { propertyIteratorSpec, } = require("resource://devtools/shared/specs/property-iterator.js"); const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); loader.lazyRequireGetter( this, "ObjectUtils", "resource://devtools/server/actors/object/utils.js" ); /** * Creates an actor to iterate over an object's property names and values. * * @param objectActor ObjectActor * The object actor. * @param options Object * A dictionary object with various boolean attributes: * - enumEntries Boolean * If true, enumerates the entries of a Map or Set object * instead of enumerating properties. * - ignoreIndexedProperties Boolean * If true, filters out Array items. * e.g. properties names between `0` and `object.length`. * - ignoreNonIndexedProperties Boolean * If true, filters out items that aren't array items * e.g. properties names that are not a number between `0` * and `object.length`. * - sort Boolean * If true, the iterator will sort the properties by name * before dispatching them. * - query String * If non-empty, will filter the properties by names and values * containing this query string. The match is not case-sensitive. * Regarding value filtering it just compare to the stringification * of the property value. */ class PropertyIteratorActor extends Actor { constructor(objectActor, options, conn) { super(conn, propertyIteratorSpec); if (!DevToolsUtils.isSafeDebuggerObject(objectActor.obj)) { this.iterator = { size: 0, propertyName: index => undefined, propertyDescription: index => undefined, }; } else if (options.enumEntries) { const cls = objectActor.obj.class; if (cls == "Map") { this.iterator = enumMapEntries(objectActor); } else if (cls == "WeakMap") { this.iterator = enumWeakMapEntries(objectActor); } else if (cls == "Set") { this.iterator = enumSetEntries(objectActor); } else if (cls == "WeakSet") { this.iterator = enumWeakSetEntries(objectActor); } else if (cls == "Storage") { this.iterator = enumStorageEntries(objectActor); } else if (cls == "URLSearchParams") { this.iterator = enumURLSearchParamsEntries(objectActor); } else if (cls == "Headers") { this.iterator = enumHeadersEntries(objectActor); } else if (cls == "HighlightRegistry") { this.iterator = enumHighlightRegistryEntries(objectActor); } else if (cls == "FormData") { this.iterator = enumFormDataEntries(objectActor); } else if (cls == "MIDIInputMap") { this.iterator = enumMidiInputMapEntries(objectActor); } else if (cls == "MIDIOutputMap") { this.iterator = enumMidiOutputMapEntries(objectActor); } else if (cls == "CustomStateSet") { this.iterator = enumCustomStateSetEntries(objectActor); } else { throw new Error( "Unsupported class to enumerate entries from: " + cls ); } } else if ( ObjectUtils.isArray(objectActor.obj) && options.ignoreNonIndexedProperties && !options.query ) { this.iterator = enumArrayProperties(objectActor, options); } else { this.iterator = enumObjectProperties(objectActor, options); } } form() { return { type: this.typeName, actor: this.actorID, count: this.iterator.size, }; } names({ indexes }) { const list = []; for (const idx of indexes) { list.push(this.iterator.propertyName(idx)); } return indexes; } slice({ start, count }) { const ownProperties = Object.create(null); for (let i = start, m = start + count; i < m; i++) { const name = this.iterator.propertyName(i); ownProperties[name] = this.iterator.propertyDescription(i); } return { ownProperties, }; } all() { return this.slice({ start: 0, count: this.iterator.size }); } } function waiveXrays(obj) { return isWorker ? obj : Cu.waiveXrays(obj); } function unwaiveXrays(obj) { return isWorker ? obj : Cu.unwaiveXrays(obj); } /** * Helper function to create a grip from a Map/Set entry */ function gripFromEntry({ obj, hooks }, entry) { entry = unwaiveXrays(entry); return hooks.createValueGrip( ObjectUtils.makeDebuggeeValueIfNeeded(obj, entry) ); } function enumArrayProperties(objectActor, options) { return { size: ObjectUtils.getArrayLength(objectActor.obj), propertyName(index) { return index; }, propertyDescription(index) { return objectActor._propertyDescriptor(index); }, }; } function enumObjectProperties(objectActor, options) { let names = []; try { names = objectActor.obj.getOwnPropertyNames(); } catch (ex) { // Calling getOwnPropertyNames() on some wrapped native prototypes is not // allowed: "cannot modify properties of a WrappedNative". See bug 952093. } if (options.ignoreNonIndexedProperties || options.ignoreIndexedProperties) { const length = DevToolsUtils.getProperty(objectActor.obj, "length"); let sliceIndex; const isLengthTrustworthy = isUint32(length) && (!length || ObjectUtils.isArrayIndex(names[length - 1])) && !ObjectUtils.isArrayIndex(names[length]); if (!isLengthTrustworthy) { // The length property may not reflect what the object looks like, let's find // where indexed properties end. if (!ObjectUtils.isArrayIndex(names[0])) { // If the first item is not a number, this means there is no indexed properties // in this object. sliceIndex = 0; } else { sliceIndex = names.length; while (sliceIndex > 0) { if (ObjectUtils.isArrayIndex(names[sliceIndex - 1])) { break; } sliceIndex--; } } } else { sliceIndex = length; } // It appears that getOwnPropertyNames always returns indexed properties // first, so we can safely slice `names` for/against indexed properties. // We do such clever operation to optimize very large array inspection. if (options.ignoreIndexedProperties) { // Keep items after `sliceIndex` index names = names.slice(sliceIndex); } else if (options.ignoreNonIndexedProperties) { // Keep `sliceIndex` first items names.length = sliceIndex; } } const safeGetterValues = objectActor._findSafeGetterValues(names); const safeGetterNames = Object.keys(safeGetterValues); // Merge the safe getter values into the existing properties list. for (const name of safeGetterNames) { if (!names.includes(name)) { names.push(name); } } if (options.query) { let { query } = options; query = query.toLowerCase(); names = names.filter(name => { // Filter on attribute names if (name.toLowerCase().includes(query)) { return true; } // and then on attribute values let desc; try { desc = objectActor.obj.getOwnPropertyDescriptor(name); } catch (e) { // Calling getOwnPropertyDescriptor on wrapped native prototypes is not // allowed (bug 560072). } if (desc?.value && String(desc.value).includes(query)) { return true; } return false; }); } if (options.sort) { names.sort(); } return { size: names.length, propertyName(index) { return names[index]; }, propertyDescription(index) { const name = names[index]; let desc = objectActor._propertyDescriptor(name); if (!desc) { desc = safeGetterValues[name]; } else if (name in safeGetterValues) { // Merge the safe getter values into the existing properties list. const { getterValue, getterPrototypeLevel } = safeGetterValues[name]; desc.getterValue = getterValue; desc.getterPrototypeLevel = getterPrototypeLevel; } return desc; }, }; } function getMapEntries(obj) { // Iterating over a Map via .entries goes through various intermediate // objects - an Iterator object, then a 2-element Array object, then the // actual values we care about. We don't have Xrays to Iterator objects, // so we get Opaque wrappers for them. And even though we have Xrays to // Arrays, the semantics often deny access to the entires based on the // nature of the values. So we need waive Xrays for the iterator object // and the tupes, and then re-apply them on the underlying values until // we fix bug 1023984. // // Even then though, we might want to continue waiving Xrays here for the // same reason we do so for Arrays above - this filtering behavior is likely // to be more confusing than beneficial in the case of Object previews. const raw = obj.unsafeDereference(); const iterator = obj.makeDebuggeeValue( waiveXrays(Map.prototype.keys.call(raw)) ); return [...DevToolsUtils.makeDebuggeeIterator(iterator)].map(k => { const key = waiveXrays(ObjectUtils.unwrapDebuggeeValue(k)); const value = Map.prototype.get.call(raw, key); return [key, value]; }); } function enumMapEntries(objectActor) { const entries = getMapEntries(objectActor.obj); return { [Symbol.iterator]: function*() { for (const [key, value] of entries) { yield [key, value].map(val => gripFromEntry(objectActor, val)); } }, size: entries.length, propertyName(index) { return index; }, propertyDescription(index) { const [key, val] = entries[index]; return { enumerable: true, value: { type: "mapEntry", preview: { key: gripFromEntry(objectActor, key), value: gripFromEntry(objectActor, val), }, }, }; }, }; } function enumStorageEntries(objectActor) { // Iterating over local / sessionStorage entries goes through various // intermediate objects - an Iterator object, then a 2-element Array object, // then the actual values we care about. We don't have Xrays to Iterator // objects, so we get Opaque wrappers for them. const raw = objectActor.obj.unsafeDereference(); const keys = []; for (let i = 0; i < raw.length; i++) { keys.push(raw.key(i)); } return { [Symbol.iterator]: function*() { for (const key of keys) { const value = raw.getItem(key); yield [key, value].map(val => gripFromEntry(objectActor, val)); } }, size: keys.length, propertyName(index) { return index; }, propertyDescription(index) { const key = keys[index]; const val = raw.getItem(key); return { enumerable: true, value: { type: "storageEntry", preview: { key: gripFromEntry(objectActor, key), value: gripFromEntry(objectActor, val), }, }, }; }, }; } function enumURLSearchParamsEntries(objectActor) { let obj = objectActor.obj; let raw = obj.unsafeDereference(); const entries = [...waiveXrays(URLSearchParams.prototype.entries.call(raw))]; return { [Symbol.iterator]: function*() { for (const [key, value] of entries) { yield [key, value]; } }, size: entries.length, propertyName(index) { // UrlSearchParams entries can have the same key multiple times (e.g. `?a=1&a=2`), // so let's return the index as a name to be able to display them properly in the client. return index; }, propertyDescription(index) { const [key, value] = entries[index]; return { enumerable: true, value: { type: "urlSearchParamsEntry", preview: { key: gripFromEntry(objectActor, key), value: gripFromEntry(objectActor, value), }, }, }; }, }; } function enumFormDataEntries(objectActor) { let obj = objectActor.obj; let raw = obj.unsafeDereference(); const entries = [...waiveXrays(FormData.prototype.entries.call(raw))]; return { [Symbol.iterator]: function*() { for (const [key, value] of entries) { yield [key, value]; } }, size: entries.length, propertyName(index) { return index; }, propertyDescription(index) { const [key, value] = entries[index]; return { enumerable: true, value: { type: "formDataEntry", preview: { key: gripFromEntry(objectActor, key), value: gripFromEntry(objectActor, value), }, }, }; }, }; } function enumHeadersEntries(objectActor) { let raw = objectActor.obj.unsafeDereference(); const entries = [...waiveXrays(Headers.prototype.entries.call(raw))]; return { [Symbol.iterator]: function*() { for (const [key, value] of entries) { yield [key, value]; } }, size: entries.length, propertyName(index) { return entries[index][0]; }, propertyDescription(index) { return { enumerable: true, value: gripFromEntry(objectActor, entries[index][1]), }; }, }; } function enumHighlightRegistryEntries(objectActor) { const entriesFuncDbgObj = objectActor.obj.getProperty("entries").return; const entriesDbgObj = entriesFuncDbgObj ? entriesFuncDbgObj.call(objectActor.obj).return : null; const entries = entriesDbgObj ? [...waiveXrays( entriesDbgObj.unsafeDereference())] : []; return { [Symbol.iterator]: function*() { for (const [key, value] of entries) { yield [key, gripFromEntry(objectActor, value)]; } }, size: entries.length, propertyName(index) { return index; }, propertyDescription(index) { const [key, value] = entries[index]; return { enumerable: true, value: { type: "highlightRegistryEntry", preview: { key: key, value: gripFromEntry(objectActor, value), }, }, }; }, }; } function enumMidiInputMapEntries(objectActor) { let raw = objectActor.obj.unsafeDereference(); // We need to waive `raw` as we can't get the iterator from the Xray for MapLike (See Bug 1173651). // We also need to waive Xrays on the result of the call to `entries` as we don't have // Xrays to Iterator objects (see Bug 1023984) const entries = Array.from( waiveXrays(MIDIInputMap.prototype.entries.call(waiveXrays(raw))) ); return { [Symbol.iterator]: function*() { for (const [key, value] of entries) { yield [key, gripFromEntry(objectActor, value)]; } }, size: entries.length, propertyName(index) { return entries[index][0]; }, propertyDescription(index) { return { enumerable: true, value: gripFromEntry(objectActor, entries[index][1]), }; }, }; } function enumMidiOutputMapEntries(objectActor) { let raw = objectActor.obj.unsafeDereference(); // We need to waive `raw` as we can't get the iterator from the Xray for MapLike (See Bug 1173651). // We also need to waive Xrays on the result of the call to `entries` as we don't have // Xrays to Iterator objects (see Bug 1023984) const entries = Array.from( waiveXrays(MIDIOutputMap.prototype.entries.call(waiveXrays(raw))) ); return { [Symbol.iterator]: function*() { for (const [key, value] of entries) { yield [key, gripFromEntry(objectActor, value)]; } }, size: entries.length, propertyName(index) { return entries[index][0]; }, propertyDescription(index) { return { enumerable: true, value: gripFromEntry(objectActor, entries[index][1]), }; }, }; } function getWeakMapEntries(obj) { // We currently lack XrayWrappers for WeakMap, so when we iterate over // the values, the temporary iterator objects get created in the target // compartment. However, we _do_ have Xrays to Object now, so we end up // Xraying those temporary objects, and filtering access to |it.value| // based on whether or not it's Xrayable and/or callable, which breaks // the for/of iteration. // // This code is designed to handle untrusted objects, so we can safely // waive Xrays on the iterable, and relying on the Debugger machinery to // make sure we handle the resulting objects carefully. const raw = obj.unsafeDereference(); const keys = waiveXrays(ChromeUtils.nondeterministicGetWeakMapKeys(raw)); return keys.map(k => [k, WeakMap.prototype.get.call(raw, k)]); } function enumWeakMapEntries(objectActor) { const entries = getWeakMapEntries(objectActor.obj); return { [Symbol.iterator]: function*() { for (let i = 0; i < entries.length; i++) { yield entries[i].map(val => gripFromEntry(objectActor, val)); } }, size: entries.length, propertyName(index) { return index; }, propertyDescription(index) { const [key, val] = entries[index]; return { enumerable: true, value: { type: "mapEntry", preview: { key: gripFromEntry(objectActor, key), value: gripFromEntry(objectActor, val), }, }, }; }, }; } function getSetValues(obj) { // We currently lack XrayWrappers for Set, so when we iterate over // the values, the temporary iterator objects get created in the target // compartment. However, we _do_ have Xrays to Object now, so we end up // Xraying those temporary objects, and filtering access to |it.value| // based on whether or not it's Xrayable and/or callable, which breaks // the for/of iteration. // // This code is designed to handle untrusted objects, so we can safely // waive Xrays on the iterable, and relying on the Debugger machinery to // make sure we handle the resulting objects carefully. const raw = obj.unsafeDereference(); const iterator = obj.makeDebuggeeValue( waiveXrays(Set.prototype.values.call(raw)) ); return [...DevToolsUtils.makeDebuggeeIterator(iterator)]; } function enumSetEntries(objectActor) { const values = getSetValues(objectActor.obj).map(v => waiveXrays(ObjectUtils.unwrapDebuggeeValue(v)) ); return { [Symbol.iterator]: function*() { for (const item of values) { yield gripFromEntry(objectActor, item); } }, size: values.length, propertyName(index) { return index; }, propertyDescription(index) { const val = values[index]; return { enumerable: true, value: gripFromEntry(objectActor, val), }; }, }; } function getWeakSetEntries(obj) { // We currently lack XrayWrappers for WeakSet, so when we iterate over // the values, the temporary iterator objects get created in the target // compartment. However, we _do_ have Xrays to Object now, so we end up // Xraying those temporary objects, and filtering access to |it.value| // based on whether or not it's Xrayable and/or callable, which breaks // the for/of iteration. // // This code is designed to handle untrusted objects, so we can safely // waive Xrays on the iterable, and relying on the Debugger machinery to // make sure we handle the resulting objects carefully. const raw = obj.unsafeDereference(); return waiveXrays(ChromeUtils.nondeterministicGetWeakSetKeys(raw)); } function enumWeakSetEntries(objectActor) { const keys = getWeakSetEntries(objectActor.obj); return { [Symbol.iterator]: function*() { for (const item of keys) { yield gripFromEntry(objectActor, item); } }, size: keys.length, propertyName(index) { return index; }, propertyDescription(index) { const val = keys[index]; return { enumerable: true, value: gripFromEntry(objectActor, val), }; }, }; } function enumCustomStateSetEntries(objectActor) { let raw = objectActor.obj.unsafeDereference(); // We need to waive `raw` as we can't get the iterator from the Xray for SetLike (See Bug 1173651). // We also need to waive Xrays on the result of the call to `values` as we don't have // Xrays to Iterator objects (see Bug 1023984) const values = Array.from( waiveXrays(CustomStateSet.prototype.values.call(waiveXrays(raw))) ); return { [Symbol.iterator]: function*() { for (const item of values) { yield gripFromEntry(objectActor, item); } }, size: values.length, propertyName(index) { return index; }, propertyDescription(index) { const val = values[index]; return { enumerable: true, value: gripFromEntry(objectActor, val), }; }, }; } /** * Returns true if the parameter can be stored as a 32-bit unsigned integer. * If so, it will be suitable for use as the length of an array object. * * @param num Number * The number to test. * @return Boolean */ function isUint32(num) { return num >>> 0 === num; } module.exports = { PropertyIteratorActor, enumCustomStateSetEntries, enumMapEntries, enumMidiInputMapEntries, enumMidiOutputMapEntries, enumSetEntries, enumURLSearchParamsEntries, enumFormDataEntries, enumHeadersEntries, enumHighlightRegistryEntries, enumWeakMapEntries, enumWeakSetEntries, };