Skip to content

Jkt/define prop fix types #1810

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions injected/integration-test/test-pages/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
<h1>Integration page</h1>
<p>This loads the injection file as if it were loaded through the content script.</p>
<script src="./build/contentScope.js"></script>
<ul>
<li><a href="./webcompat/">Web compat testing</a></li>
</ul>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"readme": "This config is used to test the device enumeration feature.",
"version": 1,
"unprotectedTemporary": [],
"features": {
"webCompat": {
"state": "enabled",
"exceptions": [],
"settings": {
"enumerateDevices": "enabled"
}
}
}
}
14 changes: 8 additions & 6 deletions injected/src/content-feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Obj, Key>} 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
Expand All @@ -247,16 +249,16 @@ 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);
return defineProperty(object, String(propertyName), /** @type {any} */ (descriptor));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions injected/src/features/gpc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions injected/src/features/navigator-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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,
Expand Down
109 changes: 56 additions & 53 deletions injected/src/features/web-compat.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// TypeScript is disabled for this file due to intentional DOM polyfills (e.g., Notification) that are incompatible with the DOM lib types.

import ContentFeature from '../content-feature.js';
// eslint-disable-next-line no-redeclare
import { URL } from '../captured-globals.js';
Expand Down Expand Up @@ -193,15 +195,16 @@ export class WebCompat extends ContentFeature {
}
// Expose the API
this.defineProperty(window, 'Notification', {
// @ts-expect-error window.Notification polyfill is intentionally incompatible with DOM lib types
value: () => {
// noop
},
writable: true,
configurable: true,
enumerable: false,
});

this.defineProperty(window.Notification, 'requestPermission', {
// window.Notification polyfill is intentionally incompatible with DOM lib types
this.defineProperty(/** @type {any} */ (window.Notification), 'requestPermission', {
value: () => {
return Promise.resolve('denied');
},
Expand All @@ -210,13 +213,13 @@ export class WebCompat extends ContentFeature {
enumerable: true,
});

this.defineProperty(window.Notification, 'permission', {
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,
Expand Down Expand Up @@ -400,6 +403,7 @@ export class WebCompat extends ContentFeature {
};
// TODO: original property is an accessor descriptor
this.defineProperty(Navigator.prototype, 'credentials', {
// @ts-expect-error validate this
value,
configurable: true,
enumerable: true,
Expand All @@ -416,6 +420,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,
Expand Down Expand Up @@ -791,65 +796,63 @@ export class WebCompat extends ContentFeature {
/**
* Creates a valid MediaDeviceInfo or InputDeviceInfo object that passes instanceof checks
* @param {'videoinput' | 'audioinput' | 'audiooutput'} kind - The device kind
* @returns {MediaDeviceInfo | InputDeviceInfo}
* @returns {MediaDeviceInfo}
*/
createMediaDeviceInfo(kind) {
// Create an empty object with the correct prototype
let deviceInfo;
const deviceInfo = /** @type {MediaDeviceInfo} */ ({});

this.defineProperty(deviceInfo, 'deviceId', {
value: 'default',
writable: false,
configurable: false,
enumerable: true
});
this.defineProperty(deviceInfo, 'kind', {
value: kind,
writable: false,
configurable: false,
enumerable: true
});
this.defineProperty(deviceInfo, 'label', {
value: '',
writable: false,
configurable: false,
enumerable: true
});
this.defineProperty(deviceInfo, 'groupId', {
value: 'default-group',
writable: false,
configurable: false,
enumerable: true
});
this.defineProperty(deviceInfo, 'toJSON', {
value: function () {
return {
deviceId: this.deviceId,
kind: this.kind,
label: this.label,
groupId: this.groupId,
};
},
writable: false,
configurable: false,
enumerable: false
});

// Set the prototype based on device type
if (kind === 'videoinput' || kind === 'audioinput') {
// Input devices should inherit from InputDeviceInfo.prototype if available
if (typeof InputDeviceInfo !== 'undefined' && InputDeviceInfo.prototype) {
deviceInfo = Object.create(InputDeviceInfo.prototype);
Object.setPrototypeOf(deviceInfo, InputDeviceInfo.prototype);
} else {
deviceInfo = Object.create(MediaDeviceInfo.prototype);
Object.setPrototypeOf(deviceInfo, MediaDeviceInfo.prototype);
}
} else {
// Output devices inherit from MediaDeviceInfo.prototype
deviceInfo = Object.create(MediaDeviceInfo.prototype);
Object.setPrototypeOf(deviceInfo, MediaDeviceInfo.prototype);
}

// Define read-only properties from the start
Object.defineProperties(deviceInfo, {
deviceId: {
value: 'default',
writable: false,
configurable: false,
enumerable: true,
},
kind: {
value: kind,
writable: false,
configurable: false,
enumerable: true,
},
label: {
value: '',
writable: false,
configurable: false,
enumerable: true,
},
groupId: {
value: 'default-group',
writable: false,
configurable: false,
enumerable: true,
},
toJSON: {
value: function () {
return {
deviceId: this.deviceId,
kind: this.kind,
label: this.label,
groupId: this.groupId,
};
},
writable: false,
configurable: false,
enumerable: true,
},
});

return deviceInfo;

return /** @type {MediaDeviceInfo} */ (deviceInfo);
}

/**
Expand Down
10 changes: 0 additions & 10 deletions injected/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,16 +458,6 @@ export class DDGProxy {
overload() {
this.objectScope[this.property] = this.internal;
}

overloadDescriptor() {
// TODO: this is not always correct! Use wrap* or shim* methods instead
this.feature.defineProperty(this.objectScope, this.property, {
value: this.internal,
writable: true,
enumerable: true,
configurable: true,
});
}
}

const maxCounter = new Map();
Expand Down
23 changes: 22 additions & 1 deletion injected/src/wrapper-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,16 +366,37 @@ 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
*/

Expand Down
Loading