Skip to content

Commit

Permalink
Add spoof-css scriptlet
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamWr committed Jan 14, 2024
1 parent 0729d65 commit 0a89055
Show file tree
Hide file tree
Showing 4 changed files with 697 additions and 0 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ 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]

### Added

- `spoof-css` scriptlet [#317](https://github.com/AdguardTeam/Scriptlets/issues/317)

## [v1.9.105] - 2023-12-25

### Added
Expand Down Expand Up @@ -326,6 +332,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 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';
222 changes: 222 additions & 0 deletions src/scriptlets/spoof-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
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[, properties[, shouldDebug]]])
* ```
*
* - `selectors` — string of comma-separated selectors to match
* - `properties` — CSS property name and property value, separated by a comma
* - `shouldDebug` — optional, defaults to `false`, if set to `true`, will trigger debugger statement
* when `getComputedStyle()` or `getBoundingClientRect()` methods is called
*
*
* ### 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 `display` to `block`, `visibility` to `visible` for all elements with class `adsbygoogle`:
*
* ```adblock
* example.org#%#//scriptlet('spoof-css', '.adsbygoogle', 'display, block, visibility, visible')
* ```
*
* 3. 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')
* ```
*
* 4. Spoof CSS property value `height` to `100`, `display` to `block` for all elements with class `adsbygoogle` and `advert`:
*
* ```adblock
* example.org#%#//scriptlet('spoof-css', '.adsbygoogle, .advert', 'height, 100, display, block')
* ```
*
* @added unknown.
*/
/* eslint-enable max-len */

export function spoofCSS(source, selectors, properties, shouldDebug = false) {
if (!selectors) {
return;
}

const arrayOfProperties = properties.replace(/\s+/g, '').split(',');

// 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
const toCamelCase = (property) => {
const toUpperCase = (text) => text.charAt(1).toUpperCase();
return property.replace(/-[a-z]/g, toUpperCase);
};

const propToValueMap = new Map();
for (let i = 0; i < arrayOfProperties.length; i += 2) {
if (arrayOfProperties[i] === '') {
break;
}
propToValueMap.set(toCamelCase(arrayOfProperties[i]), arrayOfProperties[i + 1]);
}

const spoofStyle = (cssProperty, realCssValue) => {
const property = cssProperty;
// For non existing properties return empty string
const realValue = realCssValue ?? '';
const shouldSpoof = propToValueMap.has(property);
const value = shouldSpoof ? propToValueMap.get(property) : realValue;
return value;
};

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);
const targetElements = new WeakSet(document.querySelectorAll(selectors));
if (!targetElements.has(args[0])) {
return style;
}
const proxiedStyle = new Proxy(style, {
get(target, prop, receiver) {
const CSSStyleProp = target[prop];
if (typeof CSSStyleProp === 'function') {
if (prop === 'getPropertyValue') {
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;
}
return CSSStyleProp.bind(target);
}
return spoofStyle(prop, Reflect.get(target, prop, receiver));
},
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);
const targetElements = new WeakSet(document.querySelectorAll(selectors));
if (!targetElements.has(thisArg)) {
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,
];
Loading

0 comments on commit 0a89055

Please sign in to comment.