Skip to content

Commit

Permalink
Improve spoof-css scriptlet
Browse files Browse the repository at this point in the history
Added special properties to spoof output of getBoundingClientRect().
  • Loading branch information
gorhill committed Nov 16, 2024
1 parent c8174d6 commit 5f5e3d7
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 116 deletions.
2 changes: 1 addition & 1 deletion assets/resources/run-at.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ registerScriptlet(runAt, {

/******************************************************************************/

function runAtHtmlElementFn(fn) {
export function runAtHtmlElementFn(fn) {
if ( document.documentElement ) {
fn();
return;
Expand Down
118 changes: 3 additions & 115 deletions assets/resources/scriptlets.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ import './cookie.js';
import './localstorage.js';
import './run-at.js';
import './safe-self.js';
import './spoof-css.js';

import { runAt, runAtHtmlElementFn } from './run-at.js';

import { getAllCookiesFn } from './cookie.js';
import { getAllLocalStorageFn } from './localstorage.js';
import { registeredScriptlets } from './base.js';
import { runAt } from './run-at.js';
import { safeSelf } from './safe-self.js';

// Externally added to the private namespace in which scriptlets execute.
Expand Down Expand Up @@ -3269,120 +3271,6 @@ function callNothrow(
});
}


/******************************************************************************/

builtinScriptlets.push({
name: 'spoof-css.js',
fn: spoofCSS,
dependencies: [
'safe-self.fn',
],
});
function spoofCSS(
selector,
...args
) {
if ( typeof selector !== 'string' ) { return; }
if ( selector === '' ) { return; }
const toCamelCase = s => s.replace(/-[a-z]/g, s => s.charAt(1).toUpperCase());
const propToValueMap = new Map();
for ( let i = 0; i < args.length; i += 2 ) {
if ( typeof args[i+0] !== 'string' ) { break; }
if ( args[i+0] === '' ) { break; }
if ( typeof args[i+1] !== 'string' ) { break; }
propToValueMap.set(toCamelCase(args[i+0]), args[i+1]);
}
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('spoof-css', selector, ...args);
const canDebug = scriptletGlobals.canDebug;
const shouldDebug = canDebug && propToValueMap.get('debug') || 0;
const instanceProperties = [ 'cssText', 'length', 'parentRule' ];
const spoofStyle = (prop, real) => {
const normalProp = toCamelCase(prop);
const shouldSpoof = propToValueMap.has(normalProp);
const value = shouldSpoof ? propToValueMap.get(normalProp) : real;
if ( shouldSpoof ) {
safe.uboLog(logPrefix, `Spoofing ${prop} to ${value}`);
}
return value;
};
const cloackFunc = (fn, thisArg, name) => {
const trap = fn.bind(thisArg);
Object.defineProperty(trap, 'name', { value: name });
Object.defineProperty(trap, 'toString', {
value: ( ) => `function ${name}() { [native code] }`
});
return trap;
};
self.getComputedStyle = new Proxy(self.getComputedStyle, {
apply: function(target, thisArg, args) {
// eslint-disable-next-line no-debugger
if ( shouldDebug !== 0 ) { debugger; }
const style = Reflect.apply(target, thisArg, args);
const targetElements = new WeakSet(document.querySelectorAll(selector));
if ( targetElements.has(args[0]) === false ) { return style; }
const proxiedStyle = new Proxy(style, {
get(target, prop) {
if ( typeof target[prop] === 'function' ) {
if ( prop === 'getPropertyValue' ) {
return cloackFunc(function getPropertyValue(prop) {
return spoofStyle(prop, target[prop]);
}, target, 'getPropertyValue');
}
return cloackFunc(target[prop], target, prop);
}
if ( instanceProperties.includes(prop) ) {
return Reflect.get(target, prop);
}
return spoofStyle(prop, Reflect.get(target, prop));
},
getOwnPropertyDescriptor(target, prop) {
if ( propToValueMap.has(prop) ) {
return {
configurable: true,
enumerable: true,
value: propToValueMap.get(prop),
writable: true,
};
}
return Reflect.getOwnPropertyDescriptor(target, prop);
},
});
return proxiedStyle;
},
get(target, prop) {
if ( prop === 'toString' ) {
return target.toString.bind(target);
}
return Reflect.get(target, prop);
},
});
Element.prototype.getBoundingClientRect = new Proxy(Element.prototype.getBoundingClientRect, {
apply: function(target, thisArg, args) {
// eslint-disable-next-line no-debugger
if ( shouldDebug !== 0 ) { debugger; }
const rect = Reflect.apply(target, thisArg, args);
const targetElements = new WeakSet(document.querySelectorAll(selector));
if ( targetElements.has(thisArg) === false ) { return rect; }
let { height, width } = rect;
if ( propToValueMap.has('width') ) {
width = parseFloat(propToValueMap.get('width'));
}
if ( propToValueMap.has('height') ) {
height = parseFloat(propToValueMap.get('height'));
}
return new self.DOMRect(rect.x, rect.y, width, height);
},
get(target, prop) {
if ( prop === 'toString' ) {
return target.toString.bind(target);
}
return Reflect.get(target, prop);
},
});
}

/******************************************************************************/

builtinScriptlets.push({
Expand Down
166 changes: 166 additions & 0 deletions assets/resources/spoof-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*******************************************************************************
uBlock Origin - a comprehensive, efficient content blocker
Copyright (C) 2019-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/uBlock
The scriptlets below are meant to be injected only into a
web page context.
*/

import { registerScriptlet } from './base.js';
import { safeSelf } from './safe-self.js';

/**
* @scriptlet spoof-css.js
*
* @description
* Spoof the value of CSS properties.
*
* @param selector
* A CSS selector for the element(s) to target.
*
* @param [property, value, ...]
* A list of property-value pairs of the style properties to spoof to the
* specified values.
*
* */

export function spoofCSS(
selector,
...args
) {
if ( typeof selector !== 'string' ) { return; }
if ( selector === '' ) { return; }
const toCamelCase = s => s.replace(/-[a-z]/g, s => s.charAt(1).toUpperCase());
const propToValueMap = new Map();
const privatePropToValueMap = new Map();
for ( let i = 0; i < args.length; i += 2 ) {
const prop = toCamelCase(args[i+0]);
if ( typeof prop !== 'string' ) { break; }
if ( prop === '' ) { break; }
const value = args[i+1];
if ( typeof value !== 'string' ) { break; }
if ( prop.charCodeAt(0) === 0x5F /* _ */ ) {
privatePropToValueMap.set(prop, value);
} else {
propToValueMap.set(toCamelCase(prop), value);
}
}
const safe = safeSelf();
const logPrefix = safe.makeLogPrefix('spoof-css', selector, ...args);
const instanceProperties = [ 'cssText', 'length', 'parentRule' ];
const spoofStyle = (prop, real) => {
const normalProp = toCamelCase(prop);
const shouldSpoof = propToValueMap.has(normalProp);
const value = shouldSpoof ? propToValueMap.get(normalProp) : real;
if ( shouldSpoof ) {
safe.uboLog(logPrefix, `Spoofing ${prop} to ${value}`);
}
return value;
};
const cloackFunc = (fn, thisArg, name) => {
const trap = fn.bind(thisArg);
Object.defineProperty(trap, 'name', { value: name });
Object.defineProperty(trap, 'toString', {
value: ( ) => `function ${name}() { [native code] }`
});
return trap;
};
self.getComputedStyle = new Proxy(self.getComputedStyle, {
apply: function(target, thisArg, args) {
// eslint-disable-next-line no-debugger
if ( privatePropToValueMap.has('_debug') ) { debugger; }
const style = Reflect.apply(target, thisArg, args);
const targetElements = new WeakSet(document.querySelectorAll(selector));
if ( targetElements.has(args[0]) === false ) { return style; }
const proxiedStyle = new Proxy(style, {
get(target, prop) {
if ( typeof target[prop] === 'function' ) {
if ( prop === 'getPropertyValue' ) {
return cloackFunc(function getPropertyValue(prop) {
return spoofStyle(prop, target[prop]);
}, target, 'getPropertyValue');
}
return cloackFunc(target[prop], target, prop);
}
if ( instanceProperties.includes(prop) ) {
return Reflect.get(target, prop);
}
return spoofStyle(prop, Reflect.get(target, prop));
},
getOwnPropertyDescriptor(target, prop) {
if ( propToValueMap.has(prop) ) {
return {
configurable: true,
enumerable: true,
value: propToValueMap.get(prop),
writable: true,
};
}
return Reflect.getOwnPropertyDescriptor(target, prop);
},
});
return proxiedStyle;
},
get(target, prop) {
if ( prop === 'toString' ) {
return target.toString.bind(target);
}
return Reflect.get(target, prop);
},
});
Element.prototype.getBoundingClientRect = new Proxy(Element.prototype.getBoundingClientRect, {
apply: function(target, thisArg, args) {
// eslint-disable-next-line no-debugger
if ( privatePropToValueMap.has('_debug') ) { debugger; }
const rect = Reflect.apply(target, thisArg, args);
const targetElements = new WeakSet(document.querySelectorAll(selector));
if ( targetElements.has(thisArg) === false ) { return rect; }
let { x, y, height, width } = rect;
if ( privatePropToValueMap.has('_rectx') ) {
x = parseFloat(privatePropToValueMap.get('_rectx'));
}
if ( privatePropToValueMap.has('_recty') ) {
y = parseFloat(privatePropToValueMap.get('_recty'));
}
if ( privatePropToValueMap.has('_rectw') ) {
width = parseFloat(privatePropToValueMap.get('_rectw'));
} else if ( propToValueMap.has('width') ) {
width = parseFloat(propToValueMap.get('width'));
}
if ( privatePropToValueMap.has('_recth') ) {
height = parseFloat(privatePropToValueMap.get('_recth'));
} else if ( propToValueMap.has('height') ) {
height = parseFloat(propToValueMap.get('height'));
}
return new self.DOMRect(x, y, width, height);
},
get(target, prop) {
if ( prop === 'toString' ) {
return target.toString.bind(target);
}
return Reflect.get(target, prop);
},
});
}
registerScriptlet(spoofCSS, {
name: 'spoof-css.js',
dependencies: [
safeSelf,
],
});

0 comments on commit 5f5e3d7

Please sign in to comment.