Skip to content

Commit

Permalink
AG-22559 Add spoof-css scriptlet. #317
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit 307fb6e
Merge: 4537c0a 1e27fb3
Author: Adam Wróblewski <adam@adguard.com>
Date:   Tue Jan 30 15:49:57 2024 +0100

    Merge branch 'master' into feature/AG-22559

commit 4537c0a
Author: Adam Wróblewski <adam@adguard.com>
Date:   Fri Jan 26 19:38:52 2024 +0100

    Update compatibility table

commit 242997c
Author: Adam Wróblewski <adam@adguard.com>
Date:   Fri Jan 26 18:49:44 2024 +0100

    Use matches() method instead of querySelectorAll

commit 8cce776
Author: Adam Wróblewski <adam@adguard.com>
Date:   Fri Jan 26 17:05:38 2024 +0100

    Remove debug argument

commit 5034934
Merge: 6403dc8 d71db5c
Author: Adam Wróblewski <adam@adguard.com>
Date:   Fri Jan 26 15:03:13 2024 +0100

    Merge branch 'master' into feature/AG-22559

commit 6403dc8
Author: Adam Wróblewski <adam@adguard.com>
Date:   Fri Jan 26 14:54:19 2024 +0100

    Allow only one pair of property name - value per rule

commit a12558a
Author: Adam Wróblewski <adam@adguard.com>
Date:   Fri Jan 19 15:10:55 2024 +0100

    Add JSDoc comment
    Check if cssPropertyName and cssPropertyValue are set
    Use debug instead of shouldDebug
    Use template strings
    Add convertSpoofCssArgs method to converter
    Add conversion tests

commit c7ef889
Author: Adam Wróblewski <adam@adguard.com>
Date:   Thu Jan 18 12:14:52 2024 +0100

    Use separate arguments for CSS name and CSS value
    Add support for uBO spoof-css arguments
    Get rid of excessive nesting

commit 7480077
Author: Adam Wróblewski <adam@adguard.com>
Date:   Mon Jan 15 13:31:00 2024 +0100

    Fix too long line

commit 0a89055
Author: Adam Wróblewski <adam@adguard.com>
Date:   Sun Jan 14 13:05:01 2024 +0100

    Add spoof-css scriptlet
  • Loading branch information
AdamWr committed Jan 30, 2024
1 parent 1e27fb3 commit 55678fa
Show file tree
Hide file tree
Showing 8 changed files with 809 additions and 4 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic
<!-- TODO: add @added tag to the files with specific version -->
<!-- during new scriptlets or redirects releasing -->

## Unreleased
## [Unreleased]

### Added

- `spoof-css` scriptlet [#317](https://github.com/AdguardTeam/Scriptlets/issues/317)
- New values `t`, `f`, `necessary`, `required` for `set-cookie` and `set-cookie-reload`
[#379](https://github.com/AdguardTeam/Scriptlets/issues/379)

Expand Down Expand Up @@ -333,6 +334,7 @@ prevent inline `onerror` and match `link` tag [#276](https://github.com/AdguardT
- `metrika-yandex-tag` [#254](https://github.com/AdguardTeam/Scriptlets/issues/254)
- `googlesyndication-adsbygoogle` [#252](https://github.com/AdguardTeam/Scriptlets/issues/252)

[Unreleased]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.105...HEAD
[v1.9.105]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.101...v1.9.105
[v1.9.101]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.96...v1.9.101
[v1.9.96]: https://github.com/AdguardTeam/Scriptlets/compare/v1.9.91...v1.9.96
Expand Down
1 change: 1 addition & 0 deletions scripts/compatibility-table.json
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@
"ubo": "window.name-defuser.js"
},
{
"adg": "spoof-css",
"ubo": "spoof-css.js"
},
{
Expand Down
27 changes: 26 additions & 1 deletion src/helpers/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ const UBO_NO_FETCH_IF_WILDCARD = '/^/';
const ESCAPED_COMMA_SEPARATOR = '\\,';
const COMMA_SEPARATOR = ',';

const SPOOF_CSS_METHOD = 'spoofCSS';
const REMOVE_ATTR_METHOD = 'removeAttr';
const REMOVE_CLASS_METHOD = 'removeClass';
const SPOOF_CSS_ALIASES = scriptletList[SPOOF_CSS_METHOD].names;
const REMOVE_ATTR_ALIASES = scriptletList[REMOVE_ATTR_METHOD].names;
const REMOVE_CLASS_ALIASES = scriptletList[REMOVE_CLASS_METHOD].names;
const REMOVE_ATTR_CLASS_APPLYING = ['asap', 'stay', 'complete'];
Expand Down Expand Up @@ -157,7 +159,7 @@ const validateRemoveAttrClassArgs = (parsedArgs: string[]): string[] => {
// https://github.com/AdguardTeam/Scriptlets/issues/133
const lastArg = restArgs.pop() as string; // https://github.com/microsoft/TypeScript/issues/30406
let applying;
// check the last parsed arg for matching possible 'applying' vale
// check the last parsed arg for matching possible 'applying' value
if (REMOVE_ATTR_CLASS_APPLYING.some((el) => lastArg.includes(el))) {
applying = lastArg;
} else {
Expand All @@ -180,6 +182,25 @@ const validateRemoveAttrClassArgs = (parsedArgs: string[]): string[] => {
return validArgs;
};

/**
* Convert uBO spoof-css scriptlet selectors argument to AdGuard syntax
*
* @param parsedArgs scriptlet arguments
* @returns converted args
*/
const convertSpoofCssArgs = (parsedArgs: string[]): string[] => {
const [name, selectors, ...restArgs] = parsedArgs;
// in uBO selectors are separated by escaped commas
// so it's necessary to replace it with just commas
const selector = replaceAll(
selectors,
ESCAPED_COMMA_SEPARATOR,
COMMA_SEPARATOR,
);
const convertedArgs = [name, selector, ...restArgs];
return convertedArgs;
};

/**
* Converts string of UBO scriptlet rule to AdGuard scriptlet rule
*
Expand All @@ -206,6 +227,10 @@ export const convertUboScriptletToAdg = (rule: string): string[] => {
parsedArgs = validateRemoveAttrClassArgs(parsedArgs);
}

if (SPOOF_CSS_ALIASES.includes(scriptletName)) {
parsedArgs = convertSpoofCssArgs(parsedArgs);
}

const args = parsedArgs
.map((arg, index) => {
let outputArg = arg;
Expand Down
1 change: 1 addition & 0 deletions src/scriptlets/scriptlets-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ export * from './remove-node-text';
export * from './trusted-replace-node-text';
export * from './evaldata-prune';
export * from './trusted-prune-inbound-object';
export * from './spoof-css';
259 changes: 259 additions & 0 deletions src/scriptlets/spoof-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import {
hit,
} from '../helpers/index';

/* eslint-disable max-len */
/**
* @scriptlet spoof-css
*
* @description
* Spoof CSS property value when `getComputedStyle()` or `getBoundingClientRect()` methods is called.
*
* Related UBO scriptlet:
* https://github.com/gorhill/uBlock/wiki/Resources-Library#spoof-cssjs-
*
* ### Syntax
*
* ```text
* example.org#%#//scriptlet('spoof-css', selectors, cssNameProperty, cssNameValue)
* ```
*
* - `selectors` — string of comma-separated selectors to match
* - `cssPropertyName` — CSS property name
* - `cssPropertyValue` — CSS property value
*
* > Call with `debug` as `cssPropertyName` and `truthy` value as `cssPropertyValue` will trigger debugger statement
* > when `getComputedStyle()` or `getBoundingClientRect()` methods is called.
* > It may be useful for debugging but it is not allowed for prod versions of filter lists.
*
* ### Examples
*
* 1. Spoof CSS property value `display` to `block` for all elements with class `adsbygoogle`:
*
* ```adblock
* example.org#%#//scriptlet('spoof-css', '.adsbygoogle', 'display', 'block')
* ```
*
* 2. Spoof CSS property value `height` to `100` for all elements with class `adsbygoogle` and `advert`:
*
* ```adblock
* example.org#%#//scriptlet('spoof-css', '.adsbygoogle, .advert', 'height', '100')
* ```
*
* 3. To invoke debugger statement:
*
* ```adblock
* example.org#%#//scriptlet('spoof-css', '.adsbygoogle', 'debug', 'true')
* ```
*
*
* @added unknown.
*/
/* eslint-enable max-len */

export function spoofCSS(source, selectors, cssPropertyName, cssPropertyValue) {
if (!selectors) {
return;
}

const uboAliases = [
'spoof-css.js',
'ubo-spoof-css.js',
'ubo-spoof-css',
];

/**
* getComputedStyle uses camelCase version of CSS properties
* for example, "clip-path" is displayed as "clipPath"
* so it's needed to convert CSS property to camelCase
*
* @param {string} cssProperty
* @returns {string} camelCase version of CSS property
*/
function convertToCamelCase(cssProperty) {
if (!cssProperty.includes('-')) {
return cssProperty;
}
const splittedProperty = cssProperty.split('-');
const firstPart = splittedProperty[0];
const secondPart = splittedProperty[1];
return `${firstPart}${secondPart[0].toUpperCase()}${secondPart.slice(1)}`;
}

const shouldDebug = !!(cssPropertyName === 'debug' && cssPropertyValue);

const propToValueMap = new Map();

/**
* UBO spoof-css analog has it's own args sequence:
* (selectors, ...arguments)
* arguments contains property-name/property-value pairs, all separated by commas
*
* example.com##+js(spoof-css, a[href="x.com"]\, .ads\, .bottom, clip-path, none)
* example.com##+js(spoof-css, .ad, clip-path, none, display, block)
* example.com##+js(spoof-css, .ad, debug, 1)
*/
if (uboAliases.includes(source.name)) {
const { args } = source;
let arrayOfProperties = [];
// Check if one before last argument is 'debug'
const isDebug = args.at(-2);
if (isDebug === 'debug') {
// If it's debug, then we need to skip first (selectors) and last two arguments
arrayOfProperties = args.slice(1, -2);
} else {
// If it's not debug, then we need to skip only first (selectors) argument
arrayOfProperties = args.slice(1);
}
for (let i = 0; i < arrayOfProperties.length; i += 2) {
if (arrayOfProperties[i] === '') {
break;
}
propToValueMap.set(convertToCamelCase(arrayOfProperties[i]), arrayOfProperties[i + 1]);
}
} else if (cssPropertyName && cssPropertyValue && !shouldDebug) {
propToValueMap.set(convertToCamelCase(cssPropertyName), cssPropertyValue);
}

const spoofStyle = (cssProperty, realCssValue) => {
return propToValueMap.has(cssProperty)
? propToValueMap.get(cssProperty)
: realCssValue;
};

const setRectValue = (rect, prop, value) => {
Object.defineProperty(
rect,
prop,
{
value: parseFloat(value),
},
);
};

const getter = (target, prop, receiver) => {
hit(source);
if (prop === 'toString') {
return target.toString.bind(target);
}
return Reflect.get(target, prop, receiver);
};

const getComputedStyleWrapper = (target, thisArg, args) => {
if (shouldDebug) {
debugger; // eslint-disable-line no-debugger
}
const style = Reflect.apply(target, thisArg, args);
if (!args[0].matches(selectors)) {
return style;
}
const proxiedStyle = new Proxy(style, {
get(target, prop) {
const CSSStyleProp = target[prop];

if (typeof CSSStyleProp !== 'function') {
return spoofStyle(prop, CSSStyleProp || '');
}

if (prop !== 'getPropertyValue') {
return CSSStyleProp.bind(target);
}

const getPropertyValueFunc = new Proxy(CSSStyleProp, {
apply(target, thisArg, args) {
const cssName = args[0];
const cssValue = thisArg[cssName];
return spoofStyle(cssName, cssValue);
},
get: getter,
});

return getPropertyValueFunc;
},
getOwnPropertyDescriptor(target, prop) {
if (propToValueMap.has(prop)) {
return {
configurable: true,
enumerable: true,
value: propToValueMap.get(prop),
writable: true,
};
}
return Reflect.getOwnPropertyDescriptor(target, prop);
},
});
hit(source);
return proxiedStyle;
};

const getComputedStyleHandler = {
apply: getComputedStyleWrapper,
get: getter,
};

window.getComputedStyle = new Proxy(window.getComputedStyle, getComputedStyleHandler);

const getBoundingClientRectWrapper = (target, thisArg, args) => {
if (shouldDebug) {
debugger; // eslint-disable-line no-debugger
}
const rect = Reflect.apply(target, thisArg, args);
if (!thisArg.matches(selectors)) {
return rect;
}

const {
top,
bottom,
height,
width,
left,
right,
} = rect;

const newDOMRect = new window.DOMRect(rect.x, rect.y, top, bottom, width, height, left, right);

if (propToValueMap.has('top')) {
setRectValue(newDOMRect, 'top', propToValueMap.get('top'));
}
if (propToValueMap.has('bottom')) {
setRectValue(newDOMRect, 'bottom', propToValueMap.get('bottom'));
}
if (propToValueMap.has('left')) {
setRectValue(newDOMRect, 'left', propToValueMap.get('left'));
}
if (propToValueMap.has('right')) {
setRectValue(newDOMRect, 'right', propToValueMap.get('right'));
}
if (propToValueMap.has('height')) {
setRectValue(newDOMRect, 'height', propToValueMap.get('height'));
}
if (propToValueMap.has('width')) {
setRectValue(newDOMRect, 'width', propToValueMap.get('width'));
}
hit(source);
return newDOMRect;
};

const getBoundingClientRectHandler = {
apply: getBoundingClientRectWrapper,
get: getter,
};

window.Element.prototype.getBoundingClientRect = new Proxy(
window.Element.prototype.getBoundingClientRect,
getBoundingClientRectHandler,
);
}

spoofCSS.names = [
'spoof-css',
// aliases are needed for matching the related scriptlet converted into our syntax
'spoof-css.js',
'ubo-spoof-css.js',
'ubo-spoof-css',
];

spoofCSS.injections = [
hit,
];
18 changes: 18 additions & 0 deletions tests/api/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,15 @@ describe('Test scriptlet api methods', () => {
actual: 'example.com##+js(set-session-storage-item, acceptCookies, false)',
expected: "example.com#%#//scriptlet('ubo-set-session-storage-item.js', 'acceptCookies', 'false')",
},
{
actual: 'example.com##+js(spoof-css, .advert, display, block)',
expected: "example.com#%#//scriptlet('ubo-spoof-css.js', '.advert', 'display', 'block')",
},
{
actual: 'example.com##+js(spoof-css, .adsbygoogle\\, #ads\\, .adTest, visibility, visible)',
// eslint-disable-next-line max-len
expected: "example.com#%#//scriptlet('ubo-spoof-css.js', '.adsbygoogle, #ads, .adTest', 'visibility', 'visible')",
},
];
test.each(validTestCases)('$actual', ({ actual, expected }) => {
expect(convertScriptletToAdg(actual)[0]).toStrictEqual(expected);
Expand Down Expand Up @@ -308,6 +317,15 @@ describe('Test scriptlet api methods', () => {
actual: String.raw`example.com#%#//scriptlet('adjust-setInterval', ',dataType:_', '1000', '0.02')`,
expected: String.raw`example.com##+js(nano-setInterval-booster, \,dataType:_, 1000, 0.02)`,
},
{
actual: "example.com#%#//scriptlet('spoof-css', '.advert', 'display', 'block')",
expected: 'example.com##+js(spoof-css, .advert, display, block)',
},
{
// eslint-disable-next-line max-len
actual: "example.com#%#//scriptlet('spoof-css', '.adsbygoogle, #ads, .adTest', 'visibility', 'visible')",
expected: 'example.com##+js(spoof-css, .adsbygoogle\\, #ads\\, .adTest, visibility, visible)',
},
];
test.each(testCases)('$actual', ({ actual, expected }) => {
expect(convertAdgScriptletToUbo(actual)).toStrictEqual(expected);
Expand Down

0 comments on commit 55678fa

Please sign in to comment.