diff --git a/CHANGELOG.md b/CHANGELOG.md index c0b6b950..85eca474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- ability for `prevent-element-src-loading` scriptlet to prevent inline `onerror` and match `link` tag [#276](https://github.com/AdguardTeam/Scriptlets/issues/276) +- new special value modifiers for `set-constant` [#316](https://github.com/AdguardTeam/Scriptlets/issues/316) + ### Changed - `trusted-set-cookie` and `trusted-set-cookie-reload` scriptlets to not encode cookie name and value @@ -18,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- issue with `updateTargetingFromMap()` method + in `googletagservices-gpt` redirect [#293](https://github.com/AdguardTeam/Scriptlets/issues/293) - website reloading if `$now$`/`$currentDate$` value is used in `trusted-set-cookie-reload` scriptlet [#291](https://github.com/AdguardTeam/Scriptlets/issues/291) - `getResponseHeader()` and `getAllResponseHeaders()` methods mock diff --git a/bamboo-specs/build.yaml b/bamboo-specs/build.yaml index d6900a92..75bf44c0 100644 --- a/bamboo-specs/build.yaml +++ b/bamboo-specs/build.yaml @@ -5,7 +5,7 @@ plan: key: SCRIPTLETSBUILD name: scriptlets - build variables: - dockerContainer: adguard/puppeteer-runner:8.0--1 + dockerContainer: adguard/puppeteer-runner:18.2--1 stages: - Build: diff --git a/bamboo-specs/test.yaml b/bamboo-specs/test.yaml index a9bb40d6..37eae877 100644 --- a/bamboo-specs/test.yaml +++ b/bamboo-specs/test.yaml @@ -5,7 +5,7 @@ plan: key: SCRIPTLETSTEST name: scriptlets - test new variables: - dockerPuppeteer: adguard/puppeteer-runner:8.0--1 + dockerPuppeteer: adguard/puppeteer-runner:18.2--1 stages: - Build: diff --git a/package.json b/package.json index 22d79276..5c1a208f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@adguard/scriptlets", - "version": "1.9.17", + "version": "1.9.22", "description": "AdGuard's JavaScript library of Scriptlets and Redirect resources", "scripts": { "build": "babel-node bundler.js", diff --git a/src/redirects/googletagservices-gpt.js b/src/redirects/googletagservices-gpt.js index 8b0cf68c..e9f4861a 100644 --- a/src/redirects/googletagservices-gpt.js +++ b/src/redirects/googletagservices-gpt.js @@ -84,7 +84,7 @@ export function GoogleTagServicesGpt(source) { // https://github.com/AdguardTeam/Scriptlets/issues/259 f.setAttribute('data-load-complete', true); f.setAttribute('data-google-container-id', true); - f.setAttribute('sandbox', true); + f.setAttribute('sandbox', ''); node.appendChild(f); } }; @@ -143,7 +143,7 @@ export function GoogleTagServicesGpt(source) { return [v]; } try { - return [Array.prototype.flat.call(v)[0]]; + return Array.prototype.flat.call(v); } catch { // do nothing } @@ -152,9 +152,10 @@ export function GoogleTagServicesGpt(source) { const updateTargeting = (targeting, map) => { if (typeof map === 'object') { - const entries = Object.entries(map || {}); - for (const [k, v] of entries) { - targeting.set(k, getTargetingValue(v)); + for (const key in map) { + if (Object.prototype.hasOwnProperty.call(map, key)) { + targeting.set(key, getTargetingValue(map[key])); + } } } }; diff --git a/src/scriptlets/prevent-element-src-loading.js b/src/scriptlets/prevent-element-src-loading.js index 7dc1606e..3633a99f 100644 --- a/src/scriptlets/prevent-element-src-loading.js +++ b/src/scriptlets/prevent-element-src-loading.js @@ -21,6 +21,7 @@ import { * - `script` * - `img` * - `iframe` + * - `link` * - `match` — required, string or regular expression for matching the element's URL; * * **Examples** @@ -45,6 +46,8 @@ export function preventElementSrcLoading(source, tagName, match) { img: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', // Empty h1 tag iframe: 'data:text/html;base64, PGRpdj48L2Rpdj4=', + // Empty data + link: 'data:text/plain;base64,', }; let instance; @@ -54,6 +57,8 @@ export function preventElementSrcLoading(source, tagName, match) { instance = HTMLImageElement; } else if (tagName === 'iframe') { instance = HTMLIFrameElement; + } else if (tagName === 'link') { + instance = HTMLLinkElement; } else { return; } @@ -74,7 +79,7 @@ export function preventElementSrcLoading(source, tagName, match) { }); } - const SOURCE_PROPERTY_NAME = 'src'; + const SOURCE_PROPERTY_NAME = tagName === 'link' ? 'href' : 'src'; const ONERROR_PROPERTY_NAME = 'onerror'; const searchRegexp = toRegExp(match); @@ -194,6 +199,27 @@ export function preventElementSrcLoading(source, tagName, match) { }; // eslint-disable-next-line max-len EventTarget.prototype.addEventListener = new Proxy(EventTarget.prototype.addEventListener, addEventListenerHandler); + + const preventInlineOnerror = (tagName, src) => { + window.addEventListener('error', (event) => { + if ( + !event.target + || !event.target.nodeName + || event.target.nodeName.toLowerCase() !== tagName + || !event.target.src + || !src.test(event.target.src) + ) { + return; + } + hit(source); + if (typeof event.target.onload === 'function') { + event.target.onerror = event.target.onload; + return; + } + event.target.onerror = noopFunc; + }, true); + }; + preventInlineOnerror(tagName, searchRegexp); } preventElementSrcLoading.names = [ diff --git a/src/scriptlets/set-constant.js b/src/scriptlets/set-constant.js index 837a46c2..5a58115d 100644 --- a/src/scriptlets/set-constant.js +++ b/src/scriptlets/set-constant.js @@ -1,6 +1,7 @@ import { hit, logMessage, + getNumberFromString, noopArray, noopObject, noopCallbackFunc, @@ -11,15 +12,15 @@ import { noopPromiseReject, noopPromiseResolve, getPropertyInChain, - setPropertyAccess, - toRegExp, matchStackTrace, nativeIsNaN, isEmptyObject, - getNativeRegexpTest, // following helpers should be imported and injected // because they are used by helpers above shouldAbortInlineOrInjectedScript, + getNativeRegexpTest, + setPropertyAccess, + toRegExp, } from '../helpers/index'; /* eslint-disable max-len */ @@ -65,8 +66,13 @@ import { * - `-1` — number value `-1` * - `yes` * - `no` - * - `stack` — optional, string or regular expression that must match the current function call stack trace; + * - `stack` — string or regular expression that must match the current function call stack trace, defaults to matching every call; * if regular expression is invalid it will be skipped + * - `valueWrapper` – optional, string to modify a value to be set. Possible wrappers: + * - `asFunction` – function returning value + * - `asCallback` – function returning callback, that would return value + * - `asResolved` – Promise that would resolve with value + * - `asRejected` – Promise that would reject with value * * **Examples** * ``` @@ -91,10 +97,57 @@ import { * ✔ document.third() === true // if the condition described above is met * ``` * + * ``` + * ! Any call to `document.fourth()` will return `yes` + * example.org#%#//scriptlet('set-constant', 'document.fourth', 'yes', '', 'asFunction') + * + * ✔ document.fourth() === 'yes' + * ``` + * + * ``` + * ! Any call to `document.fifth()` will return `yes` + * example.org#%#//scriptlet('set-constant', 'document.fifth', '42', '', 'asRejected') + * + * ✔ document.fifth.catch((reason) => reason === 42) // promise rejects with specified number + * ``` + * * @added v1.0.4. */ /* eslint-enable max-len */ -export function setConstant(source, property, value, stack) { +export function setConstant(source, property, value, stack = '', valueWrapper = '') { + const uboAliases = [ + 'set-constant.js', + 'ubo-set-constant.js', + 'set.js', + 'ubo-set.js', + 'ubo-set-constant', + 'ubo-set', + ]; + + /** + * UBO set-constant analog has it's own args sequence: + * (property, value, defer | wrapper) + * 'defer' – a stringified number, which defines execution time, or + * 'wrapper' - string which defines value wrapper name + * + * joysound.com##+js(set, document.body.oncopy, null, 3) + * kompetent.de##+js(set, Object.keys, 42, asFunction) + */ + if (uboAliases.includes(source.name)) { + /** + * Check that third argument was intended as 'valueWrapper' argument, + * by excluding 'defer' single digits case, and move it to 'valueWrapper' + */ + if (stack.length !== 1 && !getNumberFromString(stack)) { + valueWrapper = stack; + } + /** + * ubo doesn't support 'stack', while adg doesn't support 'defer' + * that goes in the same spot, so we discard it + */ + stack = undefined; + } + if (!property || !matchStackTrace(stack, new Error().stack)) { return; @@ -150,6 +203,32 @@ export function setConstant(source, property, value, stack) { return; } + const valueWrapperNames = [ + 'asFunction', + 'asCallback', + 'asResolved', + 'asRejected', + ]; + + if (valueWrapperNames.includes(valueWrapper)) { + const valueWrappersMap = { + asFunction(v) { + return () => v; + }, + asCallback(v) { + return () => (() => v); + }, + asResolved(v) { + return Promise.resolve(v); + }, + asRejected(v) { + return Promise.reject(v); + }, + }; + + constantValue = valueWrappersMap[valueWrapper](constantValue); + } + let canceled = false; const mustCancel = (value) => { if (canceled) { @@ -313,6 +392,7 @@ setConstant.names = [ setConstant.injections = [ hit, logMessage, + getNumberFromString, noopArray, noopObject, noopFunc, @@ -323,13 +403,13 @@ setConstant.injections = [ noopPromiseReject, noopPromiseResolve, getPropertyInChain, - setPropertyAccess, - toRegExp, matchStackTrace, nativeIsNaN, isEmptyObject, - getNativeRegexpTest, // following helpers should be imported and injected // because they are used by helpers above shouldAbortInlineOrInjectedScript, + getNativeRegexpTest, + setPropertyAccess, + toRegExp, ]; diff --git a/tests/redirects/googletagservices-gpt.test.js b/tests/redirects/googletagservices-gpt.test.js index b91e1494..f4300da1 100644 --- a/tests/redirects/googletagservices-gpt.test.js +++ b/tests/redirects/googletagservices-gpt.test.js @@ -115,7 +115,39 @@ test('Test recreateIframeForSlot', (assert) => { // https://github.com/AdguardTeam/Scriptlets/issues/259 assert.ok(iframe.getAttribute('data-load-complete'), 'attr was mocked'); assert.ok(iframe.getAttribute('data-google-container-id'), 'attr was mocked'); - assert.ok(iframe.getAttribute('sandbox'), 'attr was mocked'); + assert.strictEqual(iframe.getAttribute('sandbox'), '', 'attr was mocked'); assert.strictEqual(window.hit, 'FIRED', 'hit function was executed'); }); + +test('Test updateTargetingFromMap', (assert) => { + runRedirect(name); + + assert.ok(window.googletag, 'window.googletag have been created'); + assert.strictEqual(typeof window.googletag.defineSlot(), 'object', 'Slot has been mocked'); + + const slot = window.googletag.defineSlot('/1234567/sports', [160, 600], 'div'); + + // https://github.com/AdguardTeam/Scriptlets/issues/293 + slot.updateTargetingFromMap({ + color: 'red', + interests: ['sports', 'music', 'movies'], + }); + + assert.strictEqual( + slot.getTargeting('color')[0], + 'red', + '.getTargeting() has been mocked - color[0] = red.', + ); + assert.strictEqual( + slot.getTargeting('interests')[0], + 'sports', + '.getTargeting() has been mocked - interests[0] = sports.', + ); + assert.strictEqual( + slot.getTargeting('interests')[1], + 'music', + '.getTargeting() has been mocked - interests[1] = music.', + ); + assert.strictEqual(window.hit, 'FIRED', 'hit function was executed'); +}); diff --git a/tests/scriptlets/prevent-element-src-loading.test.js b/tests/scriptlets/prevent-element-src-loading.test.js index c79452ac..2e3ee36d 100644 --- a/tests/scriptlets/prevent-element-src-loading.test.js +++ b/tests/scriptlets/prevent-element-src-loading.test.js @@ -14,11 +14,13 @@ const afterEach = () => { if (window.elem) { window.elem.remove(); } - clearGlobalProps('hit', '__debug', 'elem'); + clearGlobalProps('hit', '__debug', 'elem', 'scriptLoaded', 'scriptBlocked'); }; const SET_SRC_ATTRIBUTE = 'setSrcAttribute'; const SET_SRC_PROP = 'srcProp'; +const SET_LINK_HREF_ATTRIBUTE = 'linkHrefAttribute'; +const SET_LINK_HREF_PROP = 'linkHrefProp'; const ONERROR_PROP = 'onerrorProp'; const ERROR_LISTENER = 'addErrorListener'; @@ -37,6 +39,18 @@ const createTestTag = (assert, nodeName, url, srcMethod, onerrorMethod) => { node.setAttribute('src', url); break; } + case SET_LINK_HREF_PROP: { + node.href = url; + node.rel = 'preload'; + node.as = 'script'; + break; + } + case SET_LINK_HREF_ATTRIBUTE: { + node.setAttribute('href', url); + node.setAttribute('rel', 'preload'); + node.setAttribute('as', 'script'); + break; + } default: // do nothing } @@ -65,10 +79,39 @@ const createTestTag = (assert, nodeName, url, srcMethod, onerrorMethod) => { return node; }; +const onErrorTestTag = (assert, url, testPassed, shouldLoad) => { + const done = assert.async(); + // Used in onload event + window.scriptLoaded = () => { + if (shouldLoad) { + testPassed = true; + assert.strictEqual(testPassed, true, 'onload event fired'); + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + } else { + assert.strictEqual(window.hit, undefined, 'hit should NOT fire'); + } + done(); + }; + // Used in onerror event + window.scriptBlocked = () => { + if (shouldLoad) { + assert.strictEqual(testPassed, true, 'onload event fired'); + assert.strictEqual(window.hit, 'FIRED', 'hit fired'); + } else { + assert.strictEqual(window.hit, undefined, 'hit should NOT fire'); + } + done(); + }; + const html = `