diff --git a/injected/integration-test/web-compat.spec.js b/injected/integration-test/web-compat.spec.js index d331c318b1..474bd436cb 100644 --- a/injected/integration-test/web-compat.spec.js +++ b/injected/integration-test/web-compat.spec.js @@ -130,6 +130,16 @@ test.describe('Ensure Notification interface is injected', () => { return window.Notification.maxActions; }); expect(maxActionsPropDenied).toEqual(2); + + const notificationToString = await page.evaluate(() => { + return window.Notification.toString(); + }); + expect(notificationToString).toEqual('function Notification() { [native code] }'); + + const requestPermissionToString = await page.evaluate(() => { + return window.Notification.requestPermission.toString(); + }); + expect(requestPermissionToString).toEqual('function requestPermission() { [native code] }'); }); }); diff --git a/injected/src/content-feature.js b/injected/src/content-feature.js index 4745c4bcf8..57b87f9a71 100644 --- a/injected/src/content-feature.js +++ b/injected/src/content-feature.js @@ -235,9 +235,11 @@ export default class ContentFeature extends ConfigFeature { /** * Define a property descriptor with debug flags. * Mainly used for defining new properties. For overriding existing properties, consider using wrapProperty(), wrapMethod() and wrapConstructor(). - * @param {any} object - object whose property we are wrapping (most commonly a prototype, e.g. globalThis.BatteryManager.prototype) - * @param {string} propertyName - * @param {import('./wrapper-utils').StrictPropertyDescriptor} descriptor - requires all descriptor options to be defined because we can't validate correctness based on TS types + * @template Obj + * @template {keyof Obj} Key + * @param {Obj} object - object whose property we are wrapping (most commonly a prototype, e.g. globalThis.BatteryManager.prototype) + * @param {Key} propertyName + * @param {import('./wrapper-utils.js').StrictPropertyDescriptorGeneric} descriptor - requires all descriptor options to be defined because we can't validate correctness based on TS types */ defineProperty(object, propertyName, descriptor) { // make sure to send a debug flag when the property is used @@ -247,16 +249,45 @@ export default class ContentFeature extends ConfigFeature { if (typeof descriptorProp === 'function') { const addDebugFlag = this.addDebugFlag.bind(this); const wrapper = new Proxy(descriptorProp, { - apply(_, thisArg, argumentsList) { + apply(target, thisArg, argumentsList) { addDebugFlag(); - return Reflect.apply(descriptorProp, thisArg, argumentsList); + return target.apply(thisArg, argumentsList); }, }); descriptor[k] = wrapToString(wrapper, descriptorProp); } }); - return defineProperty(object, propertyName, descriptor); + // Build complete strict descriptor all at once so TS doesn't complain about missing properties + let strictDescriptor; + + if (descriptor.value !== undefined || descriptor.writable !== undefined) { + // Data descriptor + strictDescriptor = { + configurable: descriptor.configurable ?? false, + enumerable: descriptor.enumerable ?? false, + value: descriptor.value, + writable: descriptor.writable ?? false, + }; + } else if (descriptor.get !== undefined || descriptor.set !== undefined) { + // Accessor descriptor + strictDescriptor = { + configurable: descriptor.configurable ?? false, + enumerable: descriptor.enumerable ?? false, + get: descriptor.get ?? (() => undefined), + set: descriptor.set ?? (() => {}), + }; + } else { + // Default data descriptor + strictDescriptor = { + configurable: descriptor.configurable ?? false, + enumerable: descriptor.enumerable ?? false, + value: undefined, + writable: false, + }; + } + + return defineProperty(object, String(propertyName), strictDescriptor); } /** diff --git a/injected/src/features/fingerprinting-temporary-storage.js b/injected/src/features/fingerprinting-temporary-storage.js index e8f57d0846..306752a310 100644 --- a/injected/src/features/fingerprinting-temporary-storage.js +++ b/injected/src/features/fingerprinting-temporary-storage.js @@ -26,6 +26,7 @@ export default class FingerprintingTemporaryStorage extends ContentFeature { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f org.call(navigator.webkitTemporaryStorage, modifiedCallback, err); }; + // @ts-expect-error This doesn't exist in the DOM lib this.defineProperty(Navigator.prototype, 'webkitTemporaryStorage', { get: () => tStorage, enumerable: true, diff --git a/injected/src/features/gpc.js b/injected/src/features/gpc.js index 2a75dc9a29..fddab8ede3 100644 --- a/injected/src/features/gpc.js +++ b/injected/src/features/gpc.js @@ -8,6 +8,7 @@ export default class GlobalPrivacyControl extends ContentFeature { if (args.globalPrivacyControlValue) { // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f if (navigator.globalPrivacyControl) return; + // @ts-expect-error This doesn't exist in the DOM lib this.defineProperty(Navigator.prototype, 'globalPrivacyControl', { get: () => true, configurable: true, @@ -18,6 +19,7 @@ export default class GlobalPrivacyControl extends ContentFeature { // this may be overwritten by the user agent or other extensions // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f if (typeof navigator.globalPrivacyControl !== 'undefined') return; + // @ts-expect-error This doesn't exist in the DOM lib this.defineProperty(Navigator.prototype, 'globalPrivacyControl', { get: () => false, configurable: true, diff --git a/injected/src/features/navigator-interface.js b/injected/src/features/navigator-interface.js index b2a83fb1af..3e95d00b87 100644 --- a/injected/src/features/navigator-interface.js +++ b/injected/src/features/navigator-interface.js @@ -24,6 +24,7 @@ export default class NavigatorInterface extends ContentFeature { if (!args.platform || !args.platform.name) { return; } + // @ts-expect-error This doesn't exist in the DOM lib this.defineProperty(Navigator.prototype, 'duckduckgo', { value: { platform: args.platform.name, diff --git a/injected/src/features/web-compat.js b/injected/src/features/web-compat.js index 0d665fc4d2..2d433c03ba 100644 --- a/injected/src/features/web-compat.js +++ b/injected/src/features/web-compat.js @@ -192,16 +192,25 @@ export class WebCompat extends ContentFeature { return; } // Expose the API + // window.Notification polyfill is intentionally incompatible with DOM lib types this.defineProperty(window, 'Notification', { - value: () => { - // noop + value: function Notification(_title, _options) { + throw new TypeError("Failed to construct 'Notification': Illegal constructor"); }, writable: true, configurable: true, enumerable: false, }); - this.defineProperty(window.Notification, 'requestPermission', { + this.defineProperty(/** @type {any} */ (window.Notification), 'toString', { + value: () => 'function Notification() { [native code] }', + writable: false, + configurable: false, + enumerable: false, + }); + + // window.Notification polyfill is intentionally incompatible with DOM lib types + this.defineProperty(/** @type {any} */ (window.Notification), 'requestPermission', { value: () => { return Promise.resolve('denied'); }, @@ -210,13 +219,20 @@ export class WebCompat extends ContentFeature { enumerable: true, }); - this.defineProperty(window.Notification, 'permission', { + this.defineProperty(/** @type {any} */ (window.Notification).requestPermission, 'toString', { + value: () => 'function requestPermission() { [native code] }', + writable: false, + configurable: false, + enumerable: false, + }); + + this.defineProperty(/** @type {any} */ (window.Notification), 'permission', { get: () => 'denied', configurable: true, enumerable: false, }); - this.defineProperty(window.Notification, 'maxActions', { + this.defineProperty(/** @type {any} */ (window.Notification), 'maxActions', { get: () => 2, configurable: true, enumerable: true, @@ -400,6 +416,7 @@ export class WebCompat extends ContentFeature { }; // TODO: original property is an accessor descriptor this.defineProperty(Navigator.prototype, 'credentials', { + // validate this value, configurable: true, enumerable: true, @@ -416,6 +433,7 @@ export class WebCompat extends ContentFeature { if (window.safari) { return; } + // @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f this.defineProperty(window, 'safari', { value: {}, writable: true, diff --git a/injected/src/wrapper-utils.js b/injected/src/wrapper-utils.js index c543edd687..1b7bdb31c5 100644 --- a/injected/src/wrapper-utils.js +++ b/injected/src/wrapper-utils.js @@ -366,16 +366,38 @@ export function shimProperty(baseObject, propertyName, implInstance, readOnly, d */ /** - * @typedef {Object} BaseStrictPropertyDescriptor + * A generic property descriptor for a property of an object, with correct `this` context for accessors. + * + * @template Obj The object type + * @template {keyof Obj} Key The property key + * @typedef {Object} StrictPropertyDescriptorGeneric * @property {boolean} configurable * @property {boolean} enumerable + * @property {boolean} [writable] + * @property {(function(this: Obj): Obj[Key]) |Obj[Key]} [value] + * @property {(function(this: Obj): Obj[Key])} [get] + * @property {(function(this: Obj, Obj[Key]): void)} [set] */ + +/** + * @typedef {Object} BaseStrictPropertyDescriptor + * @property {boolean} configurable + * @property {boolean} enumerable + * */ /** * @typedef {BaseStrictPropertyDescriptor & { value: any; writable: boolean }} StrictDataDescriptor + * */ +/** * @typedef {BaseStrictPropertyDescriptor & { get: () => any; set: (v: any) => void }} StrictAccessorDescriptor + * */ +/** * @typedef {BaseStrictPropertyDescriptor & { get: () => any }} StrictGetDescriptor + * */ +/** * @typedef {BaseStrictPropertyDescriptor & { set: (v: any) => void }} StrictSetDescriptor + * */ +/** * @typedef {StrictDataDescriptor | StrictAccessorDescriptor | StrictGetDescriptor | StrictSetDescriptor} StrictPropertyDescriptor */