|
288 | 288 | return processAttr(configSetting, defaultValue)
|
289 | 289 | }
|
290 | 290 |
|
| 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 | + |
291 | 300 | /**
|
292 | 301 | * Handles the processing of a config setting.
|
293 | 302 | * @param {*} configSetting
|
|
313 | 322 | return defaultValue
|
314 | 323 | }
|
315 | 324 |
|
| 325 | + if (configSetting.type === 'function') { |
| 326 | + if (configSetting.functionName && functionMap[configSetting.functionName]) { |
| 327 | + return functionMap[configSetting.functionName] |
| 328 | + } |
| 329 | + } |
| 330 | + |
316 | 331 | if (configSetting.type === 'undefined') {
|
317 | 332 | return undefined
|
318 | 333 | }
|
|
8657 | 8672 | init: init$3
|
8658 | 8673 | });
|
8659 | 8674 |
|
| 8675 | + /* global TrustedScriptURL, TrustedScript */ |
| 8676 | + |
8660 | 8677 | let stackDomains = [];
|
8661 | 8678 | let matchAllStackDomains = false;
|
8662 | 8679 | let taintCheck = false;
|
8663 | 8680 | let initialCreateElement;
|
8664 | 8681 | let tagModifiers = {};
|
8665 | 8682 | let shadowDomEnabled = false;
|
| 8683 | + let scriptOverload = {}; |
8666 | 8684 |
|
8667 | 8685 | /**
|
8668 | 8686 | * @param {string} tagName
|
|
8681 | 8699 | const featureName = 'runtimeChecks';
|
8682 | 8700 | const taintSymbol = Symbol(featureName);
|
8683 | 8701 | 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; |
8684 | 8711 |
|
8685 | 8712 | class DDGRuntimeChecks extends HTMLElement {
|
8686 | 8713 | #tagName
|
|
8757 | 8784 | });
|
8758 | 8785 | }
|
8759 | 8786 |
|
| 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 | + |
8760 | 8865 | /**
|
8761 | 8866 | * The element has been moved to the DOM, so we can now reflect all changes to a real element.
|
8762 | 8867 | * This is to allow us to interrogate the real element before it is moved to the DOM.
|
|
8773 | 8878 | // Reflect all attrs to the new element
|
8774 | 8879 | for (const attribute of this.getAttributeNames()) {
|
8775 | 8880 | if (shouldFilterKey(this.#tagName, 'attribute', attribute)) continue
|
8776 |
| - el.setAttribute(attribute, this.getAttribute(attribute)); |
| 8881 | + defaultElementMethods.setAttribute.call(el, attribute, this.getAttribute(attribute)); |
8777 | 8882 | }
|
8778 | 8883 |
|
8779 | 8884 | // Reflect all props to the new element
|
|
8816 | 8921 | el.appendChild(this.firstChild);
|
8817 | 8922 | }
|
8818 | 8923 |
|
| 8924 | + if (this.#tagName === 'script') { |
| 8925 | + this.computeScriptOverload(el); |
| 8926 | + } |
| 8927 | + |
8819 | 8928 | // Move the new element to the DOM
|
8820 | 8929 | try {
|
8821 | 8930 | this.insertAdjacentElement('afterend', el);
|
|
8835 | 8944 | return this.#el?.deref()
|
8836 | 8945 | }
|
8837 | 8946 |
|
| 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 | + |
8838 | 8963 | /* Native DOM element methods we're capturing to supplant values into the constructed node or store data for. */
|
8839 | 8964 |
|
8840 | 8965 | set src (value) {
|
|
8852 | 8977 | return el.src
|
8853 | 8978 | }
|
8854 | 8979 | // @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) { |
8857 | 8981 | return this.#sinks.src.toString()
|
8858 | 8982 | }
|
8859 | 8983 | return this.#sinks.src
|
|
8864 | 8988 | if (supportedSinks.includes(name)) {
|
8865 | 8989 | return this[name]
|
8866 | 8990 | }
|
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) |
8872 | 8992 | }
|
8873 | 8993 |
|
8874 | 8994 | setAttribute (name, value) {
|
|
8877 | 8997 | this[name] = value;
|
8878 | 8998 | return
|
8879 | 8999 | }
|
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) |
8885 | 9001 | }
|
8886 | 9002 |
|
8887 | 9003 | removeAttribute (name) {
|
|
8890 | 9006 | delete this[name];
|
8891 | 9007 | return
|
8892 | 9008 | }
|
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) |
8898 | 9010 | }
|
8899 | 9011 |
|
8900 | 9012 | addEventListener (...args) {
|
|
8931 | 9043 | }
|
8932 | 9044 |
|
8933 | 9045 | remove () {
|
8934 |
| - const el = this._getElement(); |
8935 |
| - if (el) { |
8936 |
| - return el.remove() |
8937 |
| - } |
8938 |
| - return super.remove() |
| 9046 | + return this._callMethod('remove') |
8939 | 9047 | }
|
8940 | 9048 |
|
| 9049 | + // @ts-expect-error TS node return here |
8941 | 9050 | 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) |
8947 | 9052 | }
|
8948 | 9053 | }
|
8949 | 9054 |
|
|
9014 | 9119 | function load () {
|
9015 | 9120 | // This shouldn't happen, but if it does we don't want to break the page
|
9016 | 9121 | try {
|
| 9122 | + // @ts-expect-error TS node return here |
9017 | 9123 | customElements.define('ddg-runtime-checks', DDGRuntimeChecks);
|
9018 | 9124 | } catch {}
|
9019 | 9125 | }
|
|
9035 | 9141 | elementRemovalTimeout = getFeatureSetting(featureName, args, 'elementRemovalTimeout') || 1000;
|
9036 | 9142 | tagModifiers = getFeatureSetting(featureName, args, 'tagModifiers') || {};
|
9037 | 9143 | shadowDomEnabled = getFeatureSettingEnabled(featureName, args, 'shadowDom') || false;
|
| 9144 | + scriptOverload = getFeatureSetting(featureName, args, 'scriptOverload') || {}; |
9038 | 9145 |
|
9039 | 9146 | overrideCreateElement();
|
9040 | 9147 |
|
|
0 commit comments