mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
Call to postMessage was failing in the zapper when using `use_dynamic_url` in manifest. Manually overwriting the per-session dynamic hostname with the real extension id fixes this.
411 lines
13 KiB
JavaScript
411 lines
13 KiB
JavaScript
/*******************************************************************************
|
|
|
|
uBlock Origin - a comprehensive, efficient content blocker
|
|
Copyright (C) 2025-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
|
|
*/
|
|
|
|
(async ( ) => {
|
|
|
|
/******************************************************************************/
|
|
|
|
const zapper = self.uBOLZapper = self.uBOLZapper || {};
|
|
|
|
if ( zapper.injected ) { return; }
|
|
zapper.injected = true;
|
|
|
|
/******************************************************************************/
|
|
|
|
const sendMessage = msg => {
|
|
try {
|
|
chrome.runtime.sendMessage(msg).catch(( ) => { });
|
|
} catch {
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const zapperSecret = (( ) => {
|
|
let secret = String.fromCharCode((Math.random() * 26) + 97);
|
|
do {
|
|
secret += (Math.floor(Math.random() * 2147483647) + 2147483647)
|
|
.toString(36)
|
|
.slice(2);
|
|
} while ( secret.length < 8 );
|
|
return secret;
|
|
})();
|
|
|
|
const zapperCSSStyle = [
|
|
'background: transparent',
|
|
'border: 0',
|
|
'border-radius: 0',
|
|
'box-shadow: none',
|
|
'color-scheme: light dark',
|
|
'display: block',
|
|
'filter: none',
|
|
'height: 100vh',
|
|
' height: 100svh',
|
|
'left: 0',
|
|
'margin: 0',
|
|
'max-height: none',
|
|
'max-width: none',
|
|
'min-height: unset',
|
|
'min-width: unset',
|
|
'opacity: 1',
|
|
'outline: 0',
|
|
'padding: 0',
|
|
'pointer-events: auto',
|
|
'position: fixed',
|
|
'top: 0',
|
|
'transform: none',
|
|
'visibility: hidden',
|
|
'width: 100%',
|
|
'z-index: 2147483647',
|
|
''
|
|
].join(' !important;\n');
|
|
|
|
const zapperCSS = `
|
|
:root > [${zapperSecret}] {
|
|
${zapperCSSStyle}
|
|
}
|
|
:root > [${zapperSecret}-loaded] {
|
|
visibility: visible !important;
|
|
}
|
|
:root [${zapperSecret}-click] {
|
|
pointer-events: none !important;
|
|
}
|
|
`;
|
|
|
|
sendMessage({ what: 'insertCSS', css: zapperCSS });
|
|
|
|
/******************************************************************************/
|
|
|
|
const getElementBoundingClientRect = function(elem) {
|
|
let rect = typeof elem.getBoundingClientRect === 'function'
|
|
? elem.getBoundingClientRect()
|
|
: { height: 0, left: 0, top: 0, width: 0 };
|
|
|
|
// https://github.com/gorhill/uBlock/issues/1024
|
|
// Try not returning an empty bounding rect.
|
|
if ( rect.width !== 0 && rect.height !== 0 ) {
|
|
return rect;
|
|
}
|
|
if ( elem.shadowRoot instanceof DocumentFragment ) {
|
|
return getElementBoundingClientRect(elem.shadowRoot);
|
|
}
|
|
|
|
let left = rect.left,
|
|
right = left + rect.width,
|
|
top = rect.top,
|
|
bottom = top + rect.height;
|
|
|
|
for ( const child of elem.children ) {
|
|
rect = getElementBoundingClientRect(child);
|
|
if ( rect.width === 0 || rect.height === 0 ) { continue; }
|
|
if ( rect.left < left ) { left = rect.left; }
|
|
if ( rect.right > right ) { right = rect.right; }
|
|
if ( rect.top < top ) { top = rect.top; }
|
|
if ( rect.bottom > bottom ) { bottom = rect.bottom; }
|
|
}
|
|
|
|
return {
|
|
bottom,
|
|
height: bottom - top,
|
|
left,
|
|
right,
|
|
top,
|
|
width: right - left
|
|
};
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const highlightElement = function(elem) {
|
|
if ( elem !== true ) {
|
|
if ( elem !== undefined ) {
|
|
if ( elem === highlightElement.current ) { return; }
|
|
if ( elem !== zapperFrame ) {
|
|
highlightElement.current = elem;
|
|
}
|
|
}
|
|
}
|
|
elem = highlightElement.current;
|
|
|
|
const ow = self.innerWidth;
|
|
const oh = self.innerHeight;
|
|
const islands = [];
|
|
|
|
if ( elem !== null ) {
|
|
const rect = getElementBoundingClientRect(elem);
|
|
// Ignore offscreen areas
|
|
if (
|
|
rect.left <= ow && rect.top <= oh &&
|
|
rect.left + rect.width >= 0 && rect.top + rect.height >= 0
|
|
) {
|
|
islands.push(
|
|
`M${rect.left} ${rect.top}h${rect.width}v${rect.height}h-${rect.width}z`
|
|
);
|
|
}
|
|
}
|
|
|
|
zapperFramePort.postMessage({
|
|
what: 'svgPaths',
|
|
ocean: `M0 0h${ow}v${oh}h-${ow}z`,
|
|
islands: islands.join(''),
|
|
});
|
|
};
|
|
highlightElement.current = null;
|
|
|
|
/******************************************************************************/
|
|
|
|
const elementFromPoint = (( ) => {
|
|
let lastX, lastY;
|
|
|
|
return (x, y) => {
|
|
if ( x !== undefined ) {
|
|
lastX = x; lastY = y;
|
|
} else if ( lastX !== undefined ) {
|
|
x = lastX; y = lastY;
|
|
} else {
|
|
return null;
|
|
}
|
|
if ( !zapperFrame ) { return null; }
|
|
const magicAttr = `${zapperSecret}-click`;
|
|
zapperFrame.setAttribute(magicAttr, '');
|
|
let elem = document.elementFromPoint(x, y);
|
|
if ( elem === document.body || elem === document.documentElement ) {
|
|
elem = null;
|
|
}
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/380
|
|
zapperFrame.removeAttribute(magicAttr);
|
|
return elem;
|
|
};
|
|
})();
|
|
|
|
/******************************************************************************/
|
|
|
|
const highlightElementAtPoint = function(mx, my) {
|
|
const elem = elementFromPoint(mx, my);
|
|
highlightElement(elem);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// https://www.reddit.com/r/uBlockOrigin/comments/bktxtb/scrolling_doesnt_work/emn901o
|
|
// Override 'fixed' position property on body element if present.
|
|
|
|
// With touch-driven devices, first highlight the element and remove only
|
|
// when tapping again the highlighted area.
|
|
|
|
const zapElementAtPoint = function(mx, my, options) {
|
|
if ( options.highlight ) {
|
|
const elem = elementFromPoint(mx, my);
|
|
if ( elem ) {
|
|
highlightElement(elem);
|
|
}
|
|
return;
|
|
}
|
|
|
|
let elemToRemove = highlightElement.current;
|
|
if ( elemToRemove === null && mx !== undefined ) {
|
|
elemToRemove = elementFromPoint(mx, my);
|
|
}
|
|
|
|
if ( elemToRemove instanceof Element === false ) { return; }
|
|
|
|
const getStyleValue = (elem, prop) => {
|
|
const style = window.getComputedStyle(elem);
|
|
return style ? style[prop] : '';
|
|
};
|
|
|
|
// Heuristic to detect scroll-locking: remove such lock when detected.
|
|
let maybeScrollLocked = elemToRemove.shadowRoot instanceof DocumentFragment;
|
|
if ( maybeScrollLocked === false ) {
|
|
let elem = elemToRemove;
|
|
do {
|
|
maybeScrollLocked =
|
|
parseInt(getStyleValue(elem, 'zIndex'), 10) >= 1000 ||
|
|
getStyleValue(elem, 'position') === 'fixed';
|
|
elem = elem.parentElement;
|
|
} while ( elem !== null && maybeScrollLocked === false );
|
|
}
|
|
if ( maybeScrollLocked ) {
|
|
const doc = document;
|
|
if ( getStyleValue(doc.body, 'overflowY') === 'hidden' ) {
|
|
doc.body.style.setProperty('overflow', 'auto', 'important');
|
|
}
|
|
if ( getStyleValue(doc.body, 'position') === 'fixed' ) {
|
|
doc.body.style.setProperty('position', 'initial', 'important');
|
|
}
|
|
if ( getStyleValue(doc.documentElement, 'position') === 'fixed' ) {
|
|
doc.documentElement.style.setProperty('position', 'initial', 'important');
|
|
}
|
|
if ( getStyleValue(doc.documentElement, 'overflowY') === 'hidden' ) {
|
|
doc.documentElement.style.setProperty('overflow', 'auto', 'important');
|
|
}
|
|
}
|
|
elemToRemove.remove();
|
|
highlightElementAtPoint(mx, my);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const onKeyPressed = function(ev) {
|
|
// Delete
|
|
if (
|
|
(ev.key === 'Delete' || ev.key === 'Backspace') ) {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
zapElementAtPoint();
|
|
return;
|
|
}
|
|
// Esc
|
|
if ( ev.key === 'Escape' || ev.which === 27 ) {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
quitZapper();
|
|
return;
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// https://github.com/chrisaljoudi/uBlock/issues/190
|
|
// May need to dynamically adjust the height of the overlay + new position
|
|
// of highlighted elements.
|
|
|
|
const onViewportChanged = function() {
|
|
highlightElement(true);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const startZapper = function() {
|
|
zapperFrame.focus();
|
|
self.addEventListener('scroll', onViewportChanged, { passive: true });
|
|
self.addEventListener('resize', onViewportChanged, { passive: true });
|
|
self.addEventListener('keydown', onKeyPressed, true);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const quitZapper = function() {
|
|
self.removeEventListener('scroll', onViewportChanged, { passive: true });
|
|
self.removeEventListener('resize', onViewportChanged, { passive: true });
|
|
self.removeEventListener('keydown', onKeyPressed, true);
|
|
if ( zapperFramePort ) {
|
|
zapperFramePort.close();
|
|
zapperFramePort.onmessage = null;
|
|
zapperFramePort.onmessageerror = null;
|
|
zapperFramePort = null;
|
|
}
|
|
if ( zapperFrame ) {
|
|
zapperFrame.remove();
|
|
zapperFrame = null;
|
|
}
|
|
sendMessage({ what: 'removeCSS', css: zapperCSS });
|
|
zapper.injected = false;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const onFrameMessage = function(msg) {
|
|
switch ( msg.what ) {
|
|
case 'start':
|
|
startZapper();
|
|
if ( Boolean(zapperFramePort) === false ) { break; }
|
|
highlightElement(true);
|
|
break;
|
|
case 'quitZapper':
|
|
quitZapper();
|
|
break;
|
|
case 'highlightElementAtPoint':
|
|
highlightElementAtPoint(msg.mx, msg.my);
|
|
break;
|
|
case 'unhighlight':
|
|
highlightElement(null);
|
|
break;
|
|
case 'zapElementAtPoint':
|
|
zapElementAtPoint(msg.mx, msg.my, msg.options);
|
|
if ( msg.options.highlight !== true && msg.options.stay !== true ) {
|
|
quitZapper();
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// zapper-ui.html will be injected in the page through an iframe, and
|
|
// is a sandboxed so as to prevent the page from interfering with its
|
|
// content and behavior.
|
|
//
|
|
// The purpose of zapper.js is to install the zapper UI, and wait for the
|
|
// component to establish a direct communication channel.
|
|
//
|
|
// When the zapper is installed on a page, the only change the page sees is an
|
|
// iframe with a random attribute. The page can't see the content of the
|
|
// iframe, and cannot interfere with its style properties. However the page
|
|
// can remove the iframe.
|
|
|
|
const bootstrap = async ( ) => {
|
|
const dynamicURL = new URL(chrome.runtime.getURL('/zapper-ui.html'));
|
|
return new Promise(resolve => {
|
|
const frame = document.createElement('iframe');
|
|
frame.setAttribute(zapperSecret, '');
|
|
document.documentElement.append(frame);
|
|
frame.onload = ( ) => {
|
|
frame.onload = null;
|
|
frame.setAttribute(`${zapperSecret}-loaded`, '');
|
|
const channel = new MessageChannel();
|
|
const port = channel.port1;
|
|
port.onmessage = ev => {
|
|
onFrameMessage(ev.data || {});
|
|
};
|
|
port.onmessageerror = ( ) => {
|
|
quitZapper();
|
|
};
|
|
const realURL = new URL(dynamicURL);
|
|
realURL.hostname = chrome.i18n.getMessage('@@extension_id');
|
|
frame.contentWindow.postMessage(
|
|
{ what: 'zapperStart' },
|
|
realURL.origin,
|
|
[ channel.port2 ]
|
|
);
|
|
frame.contentWindow.focus();
|
|
resolve({
|
|
zapperFrame: frame,
|
|
zapperFramePort: port,
|
|
});
|
|
};
|
|
frame.contentWindow.location = dynamicURL.href;
|
|
});
|
|
};
|
|
|
|
let { zapperFrame, zapperFramePort } = await bootstrap();
|
|
if ( zapperFrame && zapperFramePort ) { return; }
|
|
|
|
quitZapper();
|
|
|
|
/******************************************************************************/
|
|
|
|
})();
|
|
|
|
|
|
void 0;
|