/******************************************************************************* 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 ( prop === '' ) { break; } const value = args[i+1]; if ( typeof value !== 'string' ) { break; } if ( prop.charCodeAt(0) === 0x5F /* _ */ ) { privatePropToValueMap.set(prop, value); } else { propToValueMap.set(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, ], });