Skip to content

Commit d2b87dc

Browse files
Release build 4.7.0 [ci release]
1 parent 43ab6ed commit d2b87dc

File tree

15 files changed

+1260
-405
lines changed

15 files changed

+1260
-405
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444

4545
- name: Collect commit ranges
4646
run: |
47-
echo "$(git log main --since "$(git show -s --format=%ci $(git rev-list --tags --max-count=1))" --pretty='format:- %s')" > ${{ github.workspace }}-CHANGELOG.txt
47+
echo "# ${{ github.event.inputs.version }} release\n\n$(git log main --since "$(git show -s --format=%ci $(git rev-list --tags --max-count=1))" --pretty='format:- %s')" > ${{ github.workspace }}-CHANGELOG.txt
4848
4949
- name: Create Release
5050
uses: softprops/action-gh-release@v1

Sources/ContentScopeScripts/dist/contentScope.js

Lines changed: 135 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,15 @@
288288
return processAttr(configSetting, defaultValue)
289289
}
290290

291+
const functionMap = {
292+
/** Useful for debugging APIs in the wild, shouldn't be used */
293+
debug: (...args) => {
294+
console.log('debugger', ...args);
295+
// eslint-disable-next-line no-debugger
296+
debugger
297+
}
298+
};
299+
291300
/**
292301
* Handles the processing of a config setting.
293302
* @param {*} configSetting
@@ -313,6 +322,12 @@
313322
return defaultValue
314323
}
315324

325+
if (configSetting.type === 'function') {
326+
if (configSetting.functionName && functionMap[configSetting.functionName]) {
327+
return functionMap[configSetting.functionName]
328+
}
329+
}
330+
316331
if (configSetting.type === 'undefined') {
317332
return undefined
318333
}
@@ -8657,12 +8672,15 @@
86578672
init: init$3
86588673
});
86598674

8675+
/* global TrustedScriptURL, TrustedScript */
8676+
86608677
let stackDomains = [];
86618678
let matchAllStackDomains = false;
86628679
let taintCheck = false;
86638680
let initialCreateElement;
86648681
let tagModifiers = {};
86658682
let shadowDomEnabled = false;
8683+
let scriptOverload = {};
86668684

86678685
/**
86688686
* @param {string} tagName
@@ -8681,6 +8699,15 @@
86818699
const featureName = 'runtimeChecks';
86828700
const taintSymbol = Symbol(featureName);
86838701
const supportedSinks = ['src'];
8702+
// Store the original methods so we can call them without any side effects
8703+
const defaultElementMethods = {
8704+
setAttribute: HTMLElement.prototype.setAttribute,
8705+
getAttribute: HTMLElement.prototype.getAttribute,
8706+
removeAttribute: HTMLElement.prototype.removeAttribute,
8707+
remove: HTMLElement.prototype.remove,
8708+
removeChild: HTMLElement.prototype.removeChild
8709+
};
8710+
const supportedTrustedTypes = 'TrustedScriptURL' in window;
86848711

86858712
class DDGRuntimeChecks extends HTMLElement {
86868713
#tagName
@@ -8757,6 +8784,84 @@
87578784
});
87588785
}
87598786

8787+
computeScriptOverload (el) {
8788+
// Short circuit if we don't have any script text
8789+
if (el.textContent === '') return
8790+
// Short circuit if we're in a trusted script environment
8791+
// @ts-expect-error TrustedScript is not defined in the TS lib
8792+
if (supportedTrustedTypes && el.textContent instanceof TrustedScript) return
8793+
8794+
const config = scriptOverload;
8795+
const processedConfig = {};
8796+
for (const [key, value] of Object.entries(config)) {
8797+
processedConfig[key] = processAttr(value);
8798+
}
8799+
// Don't do anything if the config is empty
8800+
if (Object.keys(processedConfig).length === 0) return
8801+
8802+
/**
8803+
* @param {*} scope
8804+
* @param {Record<string, any>} outputs
8805+
* @returns {Proxy}
8806+
*/
8807+
function constructProxy (scope, outputs) {
8808+
return new Proxy(scope, {
8809+
get (target, property, receiver) {
8810+
const targetObj = target[property];
8811+
if (typeof targetObj === 'function') {
8812+
return (...args) => {
8813+
return Reflect.apply(target[property], target, args)
8814+
}
8815+
} else {
8816+
if (typeof property === 'string' && property in outputs) {
8817+
return Reflect.get(outputs, property, receiver)
8818+
}
8819+
return Reflect.get(target, property, receiver)
8820+
}
8821+
}
8822+
})
8823+
}
8824+
8825+
let prepend = '';
8826+
const aggregatedLookup = new Map();
8827+
/* Convert the config into a map of scopePath -> { key: value } */
8828+
for (const [key, value] of Object.entries(processedConfig)) {
8829+
const path = key.split('.');
8830+
const scopePath = path.slice(0, -1).join('.');
8831+
const pathOut = path[path.length - 1];
8832+
if (aggregatedLookup.has(scopePath)) {
8833+
aggregatedLookup.get(scopePath)[pathOut] = value;
8834+
} else {
8835+
aggregatedLookup.set(scopePath, {
8836+
[pathOut]: value
8837+
});
8838+
}
8839+
}
8840+
8841+
for (const [key, value] of aggregatedLookup) {
8842+
const path = key.split('.');
8843+
if (path.length !== 1) {
8844+
console.error('Invalid config, currently only one layer depth is supported');
8845+
continue
8846+
}
8847+
const scopeName = path[0];
8848+
prepend += `
8849+
let ${scopeName} = constructProxy(parentScope.${scopeName}, ${JSON.stringify(value)});
8850+
`;
8851+
}
8852+
const keysOut = [...aggregatedLookup.keys()].join(',\n');
8853+
prepend += `
8854+
const window = constructProxy(parentScope, {
8855+
${keysOut}
8856+
});
8857+
const globalThis = constructProxy(parentScope, {
8858+
${keysOut}
8859+
});
8860+
`;
8861+
const innerCode = prepend + el.textContent;
8862+
el.textContent = '(function (parentScope) {' + constructProxy.toString() + ' ' + innerCode + '})(globalThis)';
8863+
}
8864+
87608865
/**
87618866
* The element has been moved to the DOM, so we can now reflect all changes to a real element.
87628867
* This is to allow us to interrogate the real element before it is moved to the DOM.
@@ -8773,7 +8878,7 @@
87738878
// Reflect all attrs to the new element
87748879
for (const attribute of this.getAttributeNames()) {
87758880
if (shouldFilterKey(this.#tagName, 'attribute', attribute)) continue
8776-
el.setAttribute(attribute, this.getAttribute(attribute));
8881+
defaultElementMethods.setAttribute.call(el, attribute, this.getAttribute(attribute));
87778882
}
87788883

87798884
// Reflect all props to the new element
@@ -8816,6 +8921,10 @@
88168921
el.appendChild(this.firstChild);
88178922
}
88188923

8924+
if (this.#tagName === 'script') {
8925+
this.computeScriptOverload(el);
8926+
}
8927+
88198928
// Move the new element to the DOM
88208929
try {
88218930
this.insertAdjacentElement('afterend', el);
@@ -8835,6 +8944,22 @@
88358944
return this.#el?.deref()
88368945
}
88378946

8947+
/**
8948+
* Calls a method on the real element if it exists, otherwise calls the method on the DDGRuntimeChecks element.
8949+
* @template {keyof defaultElementMethods} E
8950+
* @param {E} method
8951+
* @param {...Parameters<defaultElementMethods[E]>} args
8952+
* @return {ReturnType<defaultElementMethods[E]>}
8953+
*/
8954+
_callMethod (method, ...args) {
8955+
const el = this._getElement();
8956+
if (el) {
8957+
return defaultElementMethods[method].call(el, ...args)
8958+
}
8959+
// @ts-expect-error TS doesn't like the spread operator
8960+
return super[method](...args)
8961+
}
8962+
88388963
/* Native DOM element methods we're capturing to supplant values into the constructed node or store data for. */
88398964

88408965
set src (value) {
@@ -8852,8 +8977,7 @@
88528977
return el.src
88538978
}
88548979
// @ts-expect-error TrustedScriptURL is not defined in the TS lib
8855-
// eslint-disable-next-line no-undef
8856-
if ('TrustedScriptURL' in window && this.#sinks.src instanceof TrustedScriptURL) {
8980+
if (supportedTrustedTypes && this.#sinks.src instanceof TrustedScriptURL) {
88578981
return this.#sinks.src.toString()
88588982
}
88598983
return this.#sinks.src
@@ -8864,11 +8988,7 @@
88648988
if (supportedSinks.includes(name)) {
88658989
return this[name]
88668990
}
8867-
const el = this._getElement();
8868-
if (el) {
8869-
return el.getAttribute(name)
8870-
}
8871-
return super.getAttribute(name)
8991+
return this._callMethod('getAttribute', name, value)
88728992
}
88738993

88748994
setAttribute (name, value) {
@@ -8877,11 +8997,7 @@
88778997
this[name] = value;
88788998
return
88798999
}
8880-
const el = this._getElement();
8881-
if (el) {
8882-
return el.setAttribute(name, value)
8883-
}
8884-
return super.setAttribute(name, value)
9000+
return this._callMethod('setAttribute', name, value)
88859001
}
88869002

88879003
removeAttribute (name) {
@@ -8890,11 +9006,7 @@
88909006
delete this[name];
88919007
return
88929008
}
8893-
const el = this._getElement();
8894-
if (el) {
8895-
return el.removeAttribute(name)
8896-
}
8897-
return super.removeAttribute(name)
9009+
return this._callMethod('removeAttribute', name)
88989010
}
88999011

89009012
addEventListener (...args) {
@@ -8931,19 +9043,12 @@
89319043
}
89329044

89339045
remove () {
8934-
const el = this._getElement();
8935-
if (el) {
8936-
return el.remove()
8937-
}
8938-
return super.remove()
9046+
return this._callMethod('remove')
89399047
}
89409048

9049+
// @ts-expect-error TS node return here
89419050
removeChild (child) {
8942-
const el = this._getElement();
8943-
if (el) {
8944-
return el.removeChild(child)
8945-
}
8946-
return super.removeChild(child)
9051+
return this._callMethod('removeChild', child)
89479052
}
89489053
}
89499054

@@ -9014,6 +9119,7 @@
90149119
function load () {
90159120
// This shouldn't happen, but if it does we don't want to break the page
90169121
try {
9122+
// @ts-expect-error TS node return here
90179123
customElements.define('ddg-runtime-checks', DDGRuntimeChecks);
90189124
} catch {}
90199125
}
@@ -9035,6 +9141,7 @@
90359141
elementRemovalTimeout = getFeatureSetting(featureName, args, 'elementRemovalTimeout') || 1000;
90369142
tagModifiers = getFeatureSetting(featureName, args, 'tagModifiers') || {};
90379143
shadowDomEnabled = getFeatureSettingEnabled(featureName, args, 'shadowDom') || false;
9144+
scriptOverload = getFeatureSetting(featureName, args, 'scriptOverload') || {};
90389145

90399146
overrideCreateElement();
90409147

0 commit comments

Comments
 (0)