Skip to content

Add overloading of property descriptors #588

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

Merged
merged 3 commits into from
Jun 22, 2023
Merged
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
103 changes: 102 additions & 1 deletion src/features/runtime-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import ContentFeature from '../content-feature.js'
import { DDGProxy, getStackTraceOrigins, getStack, matchHostname, injectGlobalStyles, createStyleElement, postDebugMessage, taintSymbol, hasTaintedMethod, taintedOrigins, getTabHostname, isBeingFramed } from '../utils.js'
import { defineProperty } from '../wrapper-utils.js'
import { defineProperty, wrapFunction } from '../wrapper-utils.js'
import { wrapScriptCodeOverload } from './runtime-checks/script-overload.js'
import { findClosestBreakpoint } from './runtime-checks/helpers.js'
import { Reflect } from '../captured-globals.js'
Expand Down Expand Up @@ -300,6 +300,23 @@ class DDGRuntimeChecks extends HTMLElement {
return super[method](...args)
}

_callSetter (prop, value) {
const el = this._getElement()
if (el) {
el[prop] = value
return
}
super[prop] = value
}

_callGetter (prop) {
const el = this._getElement()
if (el) {
return el[prop]
}
return super[prop]
}

/* Native DOM element methods we're capturing to supplant values into the constructed node or store data for. */

set src (value) {
Expand Down Expand Up @@ -473,6 +490,87 @@ function isInterrogatingDebugMessage (matchType, matchedStackDomain, stack, scri
})
}

function isRuntimeElement (element) {
try {
return element instanceof DDGRuntimeChecks
} catch {}
return false
}

function overloadGetOwnPropertyDescriptor () {
const capturedDescriptors = {
HTMLScriptElement: Object.getOwnPropertyDescriptors(HTMLScriptElement),
HTMLScriptElementPrototype: Object.getOwnPropertyDescriptors(HTMLScriptElement.prototype)
}
/**
* @param {any} value
* @returns {string | undefined}
*/
function getInterfaceName (value) {
let interfaceName
if (value === HTMLScriptElement) {
interfaceName = 'HTMLScriptElement'
}
if (value === HTMLScriptElement.prototype) {
interfaceName = 'HTMLScriptElementPrototype'
}
return interfaceName
}
// TODO: Consoldiate with wrapProperty code
function getInterfaceDescriptor (interfaceValue, interfaceName, propertyName) {
const capturedInterface = capturedDescriptors[interfaceName] && capturedDescriptors[interfaceName][propertyName]
const capturedInterfaceOut = { ...capturedInterface }
if (capturedInterface.get) {
capturedInterfaceOut.get = wrapFunction(function () {
let method = capturedInterface.get
if (isRuntimeElement(this)) {
method = () => this._callGetter(propertyName)
}
return method.call(this)
}, capturedInterface.get)
}
if (capturedInterface.set) {
capturedInterfaceOut.set = wrapFunction(function (value) {
let method = capturedInterface
if (isRuntimeElement(this)) {
method = (value) => this._callSetter(propertyName, value)
}
return method.call(this, [value])
}, capturedInterface.set)
}
return capturedInterfaceOut
}
const proxy = new DDGProxy(featureName, Object, 'getOwnPropertyDescriptor', {
apply (fn, scope, args) {
const interfaceValue = args[0]
const interfaceName = getInterfaceName(interfaceValue)
const propertyName = args[1]
const capturedInterface = capturedDescriptors[interfaceName] && capturedDescriptors[interfaceName][propertyName]
if (interfaceName && capturedInterface) {
return getInterfaceDescriptor(interfaceValue, interfaceName, propertyName)
}
return Reflect.apply(fn, scope, args)
}
})
proxy.overload()
const proxy2 = new DDGProxy(featureName, Object, 'getOwnPropertyDescriptors', {
apply (fn, scope, args) {
const interfaceValue = args[0]
const interfaceName = getInterfaceName(interfaceValue)
const capturedInterface = capturedDescriptors[interfaceName]
if (interfaceName && capturedInterface) {
const out = {}
for (const propertyName of Object.getOwnPropertyNames(capturedInterface)) {
out[propertyName] = getInterfaceDescriptor(interfaceValue, interfaceName, propertyName)
}
return out
}
return Reflect.apply(fn, scope, args)
}
})
proxy2.overload()
}

function overrideCreateElement (debug) {
const proxy = new DDGProxy(featureName, Document.prototype, 'createElement', {
apply (fn, scope, args) {
Expand Down Expand Up @@ -576,6 +674,9 @@ export default class RuntimeChecks extends ContentFeature {
if (this.getFeatureSettingEnabled('overloadReplaceChild')) {
overloadReplaceChild()
}
if (this.getFeatureSettingEnabled('overloadGetOwnPropertyDescriptor')) {
overloadGetOwnPropertyDescriptor()
}
}

injectGenericOverloads () {
Expand Down
27 changes: 27 additions & 0 deletions src/wrapper-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,33 @@ function wrapToString (newFn, origFn) {
}
}

/**
* Wrap functions to fix toString but also behave as closely to their real function as possible like .name and .length etc.
* TODO: validate with firefox non runtimeChecks context and also consolidate with wrapToString
* @param {*} functionValue
* @param {*} realTarget
* @returns {Proxy} a proxy for the function
*/
export function wrapFunction (functionValue, realTarget) {
return new Proxy(realTarget, {
get (target, prop, receiver) {
if (prop === 'toString') {
const method = Reflect.get(target, prop, receiver).bind(target)
Object.defineProperty(method, 'toString', {
value: functionToString.bind(functionToString),
enumerable: false
})
return method
}
return Reflect.get(target, prop, receiver)
},
apply (target, thisArg, argumentsList) {
// This is where we call our real function
return Reflect.apply(functionValue, thisArg, argumentsList)
}
})
}

/**
* Wrap a get/set or value property descriptor. Only for data properties. For methods, use wrapMethod(). For constructors, use wrapConstructor().
* @param {any} object - object whose property we are wrapping (most commonly a prototype)
Expand Down
2 changes: 1 addition & 1 deletion unit-test/verify-artifacts.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { cwd } from '../scripts/script-utils.js'
const ROOT = join(cwd(import.meta.url), '..')
const BUILD = join(ROOT, 'build')
const APPLE_BUILD = join(ROOT, 'Sources/ContentScopeScripts/dist')
let CSS_OUTPUT_SIZE = 590_000
let CSS_OUTPUT_SIZE = 600_000
const CSS_OUTPUT_SIZE_CHROME = CSS_OUTPUT_SIZE * 1.45 // 45% larger for Chrome MV2 due to base64 encoding
if (process.platform === 'win32') {
CSS_OUTPUT_SIZE = CSS_OUTPUT_SIZE * 1.1 // 10% larger for Windows due to line endings
Expand Down