Isolate DOM inspector layers from page context

Related issue:
https://github.com/uBlockOrigin/uBlock-issues/issues/1411

Additionally, refactored communication mechanism between content
script contexts and uBO contexts by using MessageChannel/BroadcastChannel
web APIs.
This commit is contained in:
Raymond Hill 2023-12-03 16:21:32 -05:00
parent 2c495487f1
commit ee83a4304a
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
16 changed files with 586 additions and 875 deletions

View File

@ -967,71 +967,11 @@ vAPI.messaging = {
}
},
broadcast: function(message) {
const messageWrapper = { broadcast: true, msg: message };
for ( const { port } of this.ports.values() ) {
try {
port.postMessage(messageWrapper);
} catch(ex) {
this.onPortDisconnect(port);
}
}
if ( this.defaultHandler ) {
this.defaultHandler(message, null, ( ) => { });
}
},
onFrameworkMessage: function(request, port, callback) {
const portDetails = this.ports.get(port.name) || {};
const tabId = portDetails.tabId;
const msg = request.msg;
switch ( msg.what ) {
case 'connectionAccepted':
case 'connectionRefused': {
const toPort = this.ports.get(msg.fromToken);
if ( toPort !== undefined ) {
msg.tabId = tabId;
toPort.port.postMessage(request);
} else {
msg.what = 'connectionBroken';
port.postMessage(request);
}
break;
}
case 'connectionRequested':
msg.tabId = tabId;
for ( const { port: toPort } of this.ports.values() ) {
if ( toPort === port ) { continue; }
try {
toPort.postMessage(request);
} catch (ex) {
this.onPortDisconnect(toPort);
}
}
break;
case 'connectionBroken':
case 'connectionCheck':
case 'connectionMessage': {
const toPort = this.ports.get(
port.name === msg.fromToken ? msg.toToken : msg.fromToken
);
if ( toPort !== undefined ) {
msg.tabId = tabId;
toPort.port.postMessage(request);
} else {
msg.what = 'connectionBroken';
port.postMessage(request);
}
break;
}
case 'extendClient':
vAPI.tabs.executeScript(tabId, {
file: '/js/vapi-client-extra.js',
frameId: portDetails.frameId,
}).then(( ) => {
callback();
});
break;
case 'localStorage': {
if ( portDetails.privileged !== true ) { break; }
const args = msg.args || [];

View File

@ -1,264 +0,0 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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
*/
// For non-background page
'use strict';
/******************************************************************************/
// Direct messaging connection ability
(( ) => {
// >>>>>>>> start of private namespace
if ( typeof vAPI !== 'object' ) { return; }
if ( vAPI.messaging instanceof Object === false ) { return; }
if ( vAPI.MessagingConnection instanceof Function ) { return; }
const listeners = new Set();
const connections = new Map();
vAPI.MessagingConnection = class {
constructor(handler, details) {
this.messaging = vAPI.messaging;
this.handler = handler;
this.id = details.id;
this.to = details.to;
this.toToken = details.toToken;
this.from = details.from;
this.fromToken = details.fromToken;
this.checkTimer = undefined;
// On Firefox it appears ports are not automatically disconnected
// when navigating to another page.
const ctor = vAPI.MessagingConnection;
if ( ctor.pagehide !== undefined ) { return; }
ctor.pagehide = ( ) => {
for ( const connection of connections.values() ) {
connection.disconnect();
connection.handler(
connection.toDetails('connectionBroken')
);
}
};
window.addEventListener('pagehide', ctor.pagehide);
}
toDetails(what, payload) {
return {
what: what,
id: this.id,
from: this.from,
fromToken: this.fromToken,
to: this.to,
toToken: this.toToken,
payload: payload
};
}
disconnect() {
if ( this.checkTimer !== undefined ) {
clearTimeout(this.checkTimer);
this.checkTimer = undefined;
}
connections.delete(this.id);
const port = this.messaging.getPort();
if ( port === null ) { return; }
port.postMessage({
channel: 'vapi',
msg: this.toDetails('connectionBroken'),
});
}
checkAsync() {
if ( this.checkTimer !== undefined ) {
clearTimeout(this.checkTimer);
}
this.checkTimer = vAPI.setTimeout(
( ) => { this.check(); },
499
);
}
check() {
this.checkTimer = undefined;
if ( connections.has(this.id) === false ) { return; }
const port = this.messaging.getPort();
if ( port === null ) { return; }
port.postMessage({
channel: 'vapi',
msg: this.toDetails('connectionCheck'),
});
this.checkAsync();
}
receive(details) {
switch ( details.what ) {
case 'connectionAccepted':
this.toToken = details.toToken;
this.handler(details);
this.checkAsync();
break;
case 'connectionBroken':
connections.delete(this.id);
this.handler(details);
break;
case 'connectionMessage':
this.handler(details);
this.checkAsync();
break;
case 'connectionCheck':
const port = this.messaging.getPort();
if ( port === null ) { return; }
if ( connections.has(this.id) ) {
this.checkAsync();
} else {
details.what = 'connectionBroken';
port.postMessage({ channel: 'vapi', msg: details });
}
break;
case 'connectionRefused':
connections.delete(this.id);
this.handler(details);
break;
}
}
send(payload) {
const port = this.messaging.getPort();
if ( port === null ) { return; }
port.postMessage({
channel: 'vapi',
msg: this.toDetails('connectionMessage', payload),
});
}
static addListener(listener) {
listeners.add(listener);
vAPI.messaging.getPort(); // Ensure a port instance exists
}
static removeListener(listener) {
listeners.delete(listener);
}
static connectTo(from, to, handler) {
const port = vAPI.messaging.getPort();
if ( port === null ) { return; }
const connection = new vAPI.MessagingConnection(handler, {
id: `${from}-${to}-${vAPI.sessionId}`,
to: to,
from: from,
fromToken: port.name
});
connections.set(connection.id, connection);
port.postMessage({
channel: 'vapi',
msg: {
what: 'connectionRequested',
id: connection.id,
from: from,
fromToken: port.name,
to: to,
}
});
return connection.id;
}
static disconnectFrom(connectionId) {
const connection = connections.get(connectionId);
if ( connection === undefined ) { return; }
connection.disconnect();
}
static sendTo(connectionId, payload) {
const connection = connections.get(connectionId);
if ( connection === undefined ) { return; }
connection.send(payload);
}
static canDestroyPort() {
return listeners.length === 0 && connections.size === 0;
}
static mustDestroyPort() {
if ( connections.size === 0 ) { return; }
for ( const connection of connections.values() ) {
connection.receive({ what: 'connectionBroken' });
}
connections.clear();
}
static canProcessMessage(details) {
if ( details.channel !== 'vapi' ) { return; }
switch ( details.msg.what ) {
case 'connectionAccepted':
case 'connectionBroken':
case 'connectionCheck':
case 'connectionMessage':
case 'connectionRefused': {
const connection = connections.get(details.msg.id);
if ( connection === undefined ) { break; }
connection.receive(details.msg);
return true;
}
case 'connectionRequested':
if ( listeners.length === 0 ) { return; }
const port = vAPI.messaging.getPort();
if ( port === null ) { break; }
let listener, result;
for ( listener of listeners ) {
result = listener(details.msg);
if ( result !== undefined ) { break; }
}
if ( result === undefined ) { break; }
if ( result === true ) {
details.msg.what = 'connectionAccepted';
details.msg.toToken = port.name;
const connection = new vAPI.MessagingConnection(
listener,
details.msg
);
connections.set(connection.id, connection);
} else {
details.msg.what = 'connectionRefused';
}
port.postMessage(details);
return true;
default:
break;
}
}
};
vAPI.messaging.extensions.push(vAPI.MessagingConnection);
// <<<<<<<< end of private namespace
})();
/******************************************************************************/
/*******************************************************************************
DO NOT:
- Remove the following code
- Add code beyond the following code
Reason:
- https://github.com/gorhill/uBlock/pull/3721
- uBO never uses the return value from injected content scripts
**/
void 0;

View File

@ -83,8 +83,6 @@ vAPI.messaging = {
port: null,
portTimer: null,
portTimerDelay: 10000,
extended: undefined,
extensions: [],
msgIdGenerator: 1,
pending: new Map(),
shuttingDown: false,
@ -127,23 +125,11 @@ vAPI.messaging = {
return;
}
}
// Unhandled messages
this.extensions.every(ext => ext.canProcessMessage(details) !== true);
},
messageListenerBound: null,
canDestroyPort: function() {
return this.pending.size === 0 && (
this.extensions.length === 0 ||
this.extensions.every(e => e.canDestroyPort())
);
},
mustDestroyPort: function() {
if ( this.extensions.length === 0 ) { return; }
this.extensions.forEach(e => e.mustDestroyPort());
this.extensions.length = 0;
return this.pending.size === 0;
},
portPoller: function() {
@ -168,7 +154,6 @@ vAPI.messaging = {
port.onDisconnect.removeListener(this.disconnectListenerBound);
this.port = null;
}
this.mustDestroyPort();
// service pending callbacks
if ( this.pending.size !== 0 ) {
const pending = this.pending;
@ -232,22 +217,6 @@ vAPI.messaging = {
port.postMessage({ channel, msgId, msg });
return promise;
},
// Dynamically extend capabilities.
//
// https://github.com/uBlockOrigin/uBlock-issues/issues/1571
// Don't use `self` to access `vAPI`.
extend: function() {
if ( this.extended === undefined ) {
this.extended = vAPI.messaging.send('vapi', {
what: 'extendClient'
}).then(( ) =>
typeof vAPI === 'object' && this.extensions.length !== 0
).catch(( ) => {
});
}
return this.extended;
},
};
vAPI.shutdown.add(( ) => {

View File

@ -53,7 +53,6 @@
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/codemirror/search.js" type="module"></script>
<script src="js/codemirror/search-thread.js"></script>

View File

@ -108,7 +108,6 @@
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/theme.js" type="module"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js" type="module"></script>

38
src/css/dom-inspector.css Normal file
View File

@ -0,0 +1,38 @@
html#ublock0-inspector,
#ublock0-inspector body {
background: transparent;
box-sizing: border-box;
height: 100vh;
margin: 0;
overflow: hidden;
width: 100vw;
}
#ublock0-inspector :focus {
outline: none;
}
#ublock0-inspector svg {
box-sizing: border-box;
height: 100%;
left: 0;
pointer-events: none;
position: fixed;
top: 0;
width: 100%;
}
#ublock0-inspector svg > path:nth-of-type(1) {
fill: rgba(255,0,0,0.2);
stroke: #F00;
}
#ublock0-inspector svg > path:nth-of-type(2) {
fill: rgba(0,255,0,0.2);
stroke: #0F0;
}
#ublock0-inspector svg > path:nth-of-type(3) {
fill: rgba(255,0,0,0.2);
stroke: #F00;
}
#ublock0-inspector svg > path:nth-of-type(4) {
fill: rgba(0,0,255,0.1);
stroke: #FFF;
stroke-width: 0.5px;
}

92
src/js/dom-inspector.js Normal file
View File

@ -0,0 +1,92 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-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
*/
'use strict';
/******************************************************************************/
/******************************************************************************/
const svgRoot = document.querySelector('svg');
const quit = ( ) => {
inspectorContentPort.postMessage({ what: 'quitInspector' });
inspectorContentPort.close();
inspectorContentPort.onmessage = inspectorContentPort.onmessageerror = null;
inspectorContentPort = undefined;
loggerPort.postMessage({ what: 'quitInspector' });
loggerPort.close();
loggerPort.onmessage = loggerPort.onmessageerror = null;
loggerPort = undefined;
};
const onMessage = (msg, fromLogger) => {
switch ( msg.what ) {
case 'quitInspector': {
quit();
break;
}
case 'svgPaths': {
const paths = svgRoot.children;
paths[0].setAttribute('d', msg.paths[0]);
paths[1].setAttribute('d', msg.paths[1]);
paths[2].setAttribute('d', msg.paths[2]);
paths[3].setAttribute('d', msg.paths[3]);
break;
}
default:
if ( typeof fromLogger !== 'boolean' ) { return; }
if ( fromLogger ) {
inspectorContentPort.postMessage(msg);
} else {
loggerPort.postMessage(msg);
}
break;
}
};
// Wait for the content script to establish communication
let inspectorContentPort;
let loggerPort = new globalThis.BroadcastChannel('loggerInspector');
loggerPort.onmessage = ev => {
const msg = ev.data || {};
onMessage(msg, true);
};
loggerPort.onmessageerror = ( ) => {
quit();
};
globalThis.addEventListener('message', ev => {
const msg = ev.data || {};
if ( msg.what !== 'startInspector' ) { return; }
if ( Array.isArray(ev.ports) === false ) { return; }
if ( ev.ports.length === 0 ) { return; }
inspectorContentPort = ev.ports[0];
inspectorContentPort.onmessage = ev => {
const msg = ev.data || {};
onMessage(msg, false);
};
inspectorContentPort.onmessageerror = ( ) => {
quit();
};
inspectorContentPort.postMessage({ what: 'startInspector' });
}, { once: true });

View File

@ -53,18 +53,15 @@ const NoPaths = 'M0 0';
const reCosmeticAnchor = /^#(\$|\?|\$\?)?#/;
const epickerId = (( ) => {
{
const url = new URL(self.location.href);
if ( url.searchParams.has('zap') ) {
pickerRoot.classList.add('zap');
}
return url.searchParams.get('epid');
})();
if ( epickerId === null ) { return; }
}
const docURL = new URL(vAPI.getURL(''));
let epickerConnectionId;
let resultsetOpt;
let netFilterCandidates = [];
@ -305,7 +302,7 @@ const cosmeticCandidatesFromFilterChoice = function(filterChoice) {
candidates.push(paths);
}
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'optimizeCandidates',
candidates,
slot,
@ -333,7 +330,7 @@ const onSvgClicked = function(ev) {
// If zap mode, highlight element under mouse, this makes the zapper usable
// on touch screens.
if ( pickerRoot.classList.contains('zap') ) {
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'zapElementAtPoint',
mx: ev.clientX,
my: ev.clientY,
@ -358,7 +355,7 @@ const onSvgClicked = function(ev) {
if ( ev.type === 'touch' ) {
pickerRoot.classList.add('show');
}
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'filterElementAtPoint',
mx: ev.clientX,
my: ev.clientY,
@ -432,7 +429,7 @@ const onSvgTouch = (( ) => {
pickerRoot.classList.contains('zap') &&
svgIslands.getAttribute('d') !== NoPaths
) {
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'unhighlight'
});
return;
@ -463,7 +460,7 @@ const onCandidateChanged = function() {
$id('resultsetModifiers').classList.toggle(
'hide', text === '' || text !== computedCandidate
);
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'dialogSetFilter',
filter,
compiled: reCosmeticAnchor.test(filter)
@ -476,7 +473,7 @@ const onCandidateChanged = function() {
const onPreviewClicked = function() {
const state = pickerRoot.classList.toggle('preview');
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'togglePreview',
state,
});
@ -496,7 +493,7 @@ const onCreateClicked = function() {
killCache: reCosmeticAnchor.test(candidate) === false,
});
}
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'dialogCreate',
filter: candidate,
compiled: reCosmeticAnchor.test(candidate)
@ -578,7 +575,7 @@ const onKeyPressed = function(ev) {
(ev.key === 'Delete' || ev.key === 'Backspace') &&
pickerRoot.classList.contains('zap')
) {
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'zapElementAtPoint',
options: { stay: true },
});
@ -678,7 +675,7 @@ const svgListening = (( ) => {
const onTimer = ( ) => {
timer = undefined;
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'highlightElementAtPoint',
mx,
my,
@ -798,7 +795,7 @@ const pausePicker = function() {
const unpausePicker = function() {
pickerRoot.classList.remove('paused', 'preview');
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerContentPort.postMessage({
what: 'togglePreview',
state: false,
});
@ -838,8 +835,9 @@ const startPicker = function() {
/******************************************************************************/
const quitPicker = function() {
vAPI.MessagingConnection.sendTo(epickerConnectionId, { what: 'quitPicker' });
vAPI.MessagingConnection.disconnectFrom(epickerConnectionId);
pickerContentPort.postMessage({ what: 'quitPicker' });
pickerContentPort.close();
pickerContentPort = undefined;
};
/******************************************************************************/
@ -876,49 +874,27 @@ const onPickerMessage = function(msg) {
/******************************************************************************/
const onConnectionMessage = function(msg) {
switch ( msg.what ) {
case 'connectionBroken':
break;
case 'connectionMessage':
onPickerMessage(msg.payload);
break;
case 'connectionAccepted':
epickerConnectionId = msg.id;
startPicker();
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
what: 'start',
});
break;
}
};
// Wait for the content script to establish communication
vAPI.MessagingConnection.connectTo(
`epickerDialog-${epickerId}`,
`epicker-${epickerId}`,
onConnectionMessage
);
let pickerContentPort;
globalThis.addEventListener('message', ev => {
const msg = ev.data || {};
if ( msg.what !== 'epickerStart' ) { return; }
if ( Array.isArray(ev.ports) === false ) { return; }
if ( ev.ports.length === 0 ) { return; }
pickerContentPort = ev.ports[0];
pickerContentPort.onmessage = ev => {
const msg = ev.data || {};
onPickerMessage(msg);
};
pickerContentPort.onmessageerror = ( ) => {
quitPicker();
};
startPicker();
pickerContentPort.postMessage({ what: 'start' });
}, { once: true });
/******************************************************************************/
})();
/*******************************************************************************
DO NOT:
- Remove the following code
- Add code beyond the following code
Reason:
- https://github.com/gorhill/uBlock/pull/3721
- uBO never uses the return value from injected content scripts
**/
void 0;

View File

@ -44,64 +44,44 @@ if (
/******************************************************************************/
const logger = self.logger;
var inspectorConnectionId;
var inspectedTabId = 0;
var inspectedURL = '';
var inspectedHostname = '';
var inspector = qs$('#domInspector');
var domTree = qs$('#domTree');
var uidGenerator = 1;
var filterToIdMap = new Map();
const inspector = qs$('#domInspector');
const domTree = qs$('#domTree');
const filterToIdMap = new Map();
let inspectedTabId = 0;
let inspectedURL = '';
let inspectedHostname = '';
let uidGenerator = 1;
/******************************************************************************/
const messaging = vAPI.messaging;
vAPI.MessagingConnection.addListener(function(msg) {
if ( msg.from !== 'domInspector' || msg.to !== 'loggerUI' ) { return; }
switch ( msg.what ) {
case 'connectionBroken':
if ( inspectorConnectionId === msg.id ) {
filterToIdMap.clear();
logger.removeAllChildren(domTree);
inspectorConnectionId = undefined;
}
injectInspector();
break;
case 'connectionMessage':
if ( msg.payload.what === 'domLayoutFull' ) {
inspectedURL = msg.payload.url;
inspectedHostname = msg.payload.hostname;
renderDOMFull(msg.payload);
} else if ( msg.payload.what === 'domLayoutIncremental' ) {
renderDOMIncremental(msg.payload);
}
break;
case 'connectionRequested':
if ( msg.tabId === undefined || msg.tabId !== inspectedTabId ) {
return;
}
filterToIdMap.clear();
logger.removeAllChildren(domTree);
inspectorConnectionId = msg.id;
return true;
const inspectorFramePort = new globalThis.BroadcastChannel('loggerInspector');
inspectorFramePort.onmessage = ev => {
const msg = ev.data || {};
if ( msg.what === 'domLayoutFull' ) {
inspectedURL = msg.url;
inspectedHostname = msg.hostname;
renderDOMFull(msg);
} else if ( msg.what === 'domLayoutIncremental' ) {
renderDOMIncremental(msg);
}
});
};
inspectorFramePort.onmessageerror = ( ) => {
};
/******************************************************************************/
const nodeFromDomEntry = function(entry) {
var node, value;
const li = document.createElement('li');
dom.attr(li, 'id', entry.nid);
// expander/collapser
li.appendChild(document.createElement('span'));
// selector
node = document.createElement('code');
let node = document.createElement('code');
node.textContent = entry.sel;
li.appendChild(node);
// descendant count
value = entry.cnt || 0;
let value = entry.cnt || 0;
node = document.createElement('span');
node.textContent = value !== 0 ? value.toLocaleString() : '';
dom.attr(node, 'data-cnt', value);
@ -114,7 +94,7 @@ const nodeFromDomEntry = function(entry) {
dom.cl.add(node, 'filter');
value = filterToIdMap.get(entry.filter);
if ( value === undefined ) {
value = uidGenerator.toString();
value = `${uidGenerator}`;
filterToIdMap.set(entry.filter, value);
uidGenerator += 1;
}
@ -142,18 +122,15 @@ const appendListItem = function(ul, li) {
/******************************************************************************/
const renderDOMFull = function(response) {
var domTreeParent = domTree.parentElement;
var ul = domTreeParent.removeChild(domTree);
const domTreeParent = domTree.parentElement;
let ul = domTreeParent.removeChild(domTree);
logger.removeAllChildren(domTree);
filterToIdMap.clear();
var lvl = 0;
var entries = response.layout;
var n = entries.length;
var li, entry;
for ( var i = 0; i < n; i++ ) {
entry = entries[i];
let lvl = 0;
let li;
for ( const entry of response.layout ) {
if ( entry.lvl === lvl ) {
li = nodeFromDomEntry(entry);
appendListItem(ul, li);
@ -186,24 +163,21 @@ const renderDOMFull = function(response) {
domTreeParent.appendChild(domTree);
};
// https://www.youtube.com/watch?v=IDGNA83mxDo
/******************************************************************************/
const patchIncremental = function(from, delta) {
var span, cnt;
var li = from.parentElement.parentElement;
var patchCosmeticHide = delta >= 0 &&
dom.cl.has(from, 'isCosmeticHide') &&
dom.cl.has(li, 'hasCosmeticHide') === false;
let li = from.parentElement.parentElement;
const patchCosmeticHide = delta >= 0 &&
dom.cl.has(from, 'isCosmeticHide') &&
dom.cl.has(li, 'hasCosmeticHide') === false;
// Include descendants count when removing a node
if ( delta < 0 ) {
delta -= countFromNode(from);
}
for ( ; li.localName === 'li'; li = li.parentElement.parentElement ) {
span = li.children[2];
const span = li.children[2];
if ( delta !== 0 ) {
cnt = countFromNode(li) + delta;
const cnt = countFromNode(li) + delta;
span.textContent = cnt !== 0 ? cnt.toLocaleString() : '';
dom.attr(span, 'data-cnt', cnt);
}
@ -219,11 +193,10 @@ const renderDOMIncremental = function(response) {
// Process each journal entry:
// 1 = node added
// -1 = node removed
var journal = response.journal;
var nodes = new Map(response.nodes);
var entry, previous, li, ul;
for ( var i = 0, n = journal.length; i < n; i++ ) {
entry = journal[i];
const nodes = new Map(response.nodes);
let li = null;
let ul = null;
for ( const entry of response.journal ) {
// Remove node
if ( entry.what === -1 ) {
li = qs$(`#${entry.nid}`);
@ -239,7 +212,7 @@ const renderDOMIncremental = function(response) {
}
// Add node as sibling
if ( entry.what === 1 && entry.l ) {
previous = qs$(`#${entry.l}`);
const previous = qs$(`#${entry.l}`);
// This should not happen
if ( previous === null ) {
// throw new Error('No left sibling!?');
@ -276,24 +249,21 @@ const renderDOMIncremental = function(response) {
/******************************************************************************/
const countFromNode = function(li) {
var span = li.children[2];
var cnt = parseInt(dom.attr(span, 'data-cnt'), 10);
const span = li.children[2];
const cnt = parseInt(dom.attr(span, 'data-cnt'), 10);
return isNaN(cnt) ? 0 : cnt;
};
/******************************************************************************/
const selectorFromNode = function(node) {
var selector = '';
var code;
let selector = '';
while ( node !== null ) {
if ( node.localName === 'li' ) {
code = qs$(node, 'code');
const code = qs$(node, 'code');
if ( code !== null ) {
selector = code.textContent + ' > ' + selector;
if ( selector.indexOf('#') !== -1 ) {
break;
}
selector = `${code.textContent} > ${selector}`;
if ( selector.includes('#') ) { break; }
}
}
node = node.parentElement;
@ -306,7 +276,7 @@ const selectorFromNode = function(node) {
const selectorFromFilter = function(node) {
while ( node !== null ) {
if ( node.localName === 'li' ) {
var code = qs$(node, 'code:nth-of-type(2)');
const code = qs$(node, 'code:nth-of-type(2)');
if ( code !== null ) {
return code.textContent;
}
@ -319,7 +289,7 @@ const selectorFromFilter = function(node) {
/******************************************************************************/
const nidFromNode = function(node) {
var li = node;
let li = node;
while ( li !== null ) {
if ( li.localName === 'li' ) {
return li.id || '';
@ -367,17 +337,17 @@ const startDialog = (function() {
};
const onClicked = function(ev) {
var target = ev.target;
const target = ev.target;
ev.stopPropagation();
if ( target.id === 'createCosmeticFilters' ) {
messaging.send('loggerUI', {
vAPI.messaging.send('loggerUI', {
what: 'createUserFilter',
filters: textarea.value,
});
// Force a reload for the new cosmetic filter(s) to take effect
messaging.send('loggerUI', {
vAPI.messaging.send('loggerUI', {
what: 'reloadTab',
tabId: inspectedTabId,
});
@ -386,7 +356,7 @@ const startDialog = (function() {
};
const showCommitted = function() {
vAPI.MessagingConnection.sendTo(inspectorConnectionId, {
inspectorFramePort.postMessage({
what: 'showCommitted',
hide: hideSelectors.join(',\n'),
unhide: unhideSelectors.join(',\n')
@ -394,7 +364,7 @@ const startDialog = (function() {
};
const showInteractive = function() {
vAPI.MessagingConnection.sendTo(inspectorConnectionId, {
inspectorFramePort.postMessage({
what: 'showInteractive',
hide: hideSelectors.join(',\n'),
unhide: unhideSelectors.join(',\n')
@ -449,8 +419,8 @@ const onClicked = function(ev) {
if ( inspectedTabId === 0 ) { return; }
var target = ev.target;
var parent = target.parentElement;
const target = ev.target;
const parent = target.parentElement;
// Expand/collapse branch
if (
@ -473,7 +443,7 @@ const onClicked = function(ev) {
// Toggle cosmetic filter
if ( dom.cl.has(target, 'filter') ) {
vAPI.MessagingConnection.sendTo(inspectorConnectionId, {
inspectorFramePort.postMessage({
what: 'toggleFilter',
original: false,
target: dom.cl.toggle(target, 'off'),
@ -489,7 +459,7 @@ const onClicked = function(ev) {
}
// Toggle node
else {
vAPI.MessagingConnection.sendTo(inspectorConnectionId, {
inspectorFramePort.postMessage({
what: 'toggleNodes',
original: true,
target: dom.cl.toggle(target, 'off') === false,
@ -509,7 +479,7 @@ const onMouseOver = (function() {
let mouseoverTarget = null;
const timerHandler = ( ) => {
vAPI.MessagingConnection.sendTo(inspectorConnectionId, {
inspectorFramePort.postMessage({
what: 'highlightOne',
selector: selectorFromNode(mouseoverTarget),
nid: nidFromNode(mouseoverTarget),
@ -544,7 +514,7 @@ const injectInspector = function() {
const tabId = currentTabId();
if ( tabId <= 0 ) { return; }
inspectedTabId = tabId;
messaging.send('loggerUI', {
vAPI.messaging.send('loggerUI', {
what: 'scriptlet',
tabId,
scriptlet: 'dom-inspector',
@ -554,9 +524,8 @@ const injectInspector = function() {
/******************************************************************************/
const shutdownInspector = function() {
if ( inspectorConnectionId !== undefined ) {
vAPI.MessagingConnection.disconnectFrom(inspectorConnectionId);
inspectorConnectionId = undefined;
if ( inspectorFramePort !== undefined ) {
inspectorFramePort.postMessage({ what: 'quitInspector' });
}
logger.removeAllChildren(domTree);
dom.cl.remove(inspector, 'vExpanded');
@ -594,10 +563,7 @@ const toggleHCompactView = function() {
const revert = function() {
dom.cl.remove('#domTree .off', 'off');
vAPI.MessagingConnection.sendTo(
inspectorConnectionId,
{ what: 'resetToggledNodes' }
);
inspectorFramePort.postMessage({ what: 'resetToggledNodes' });
dom.cl.add(qs$(inspector, '.permatoolbar .revert'), 'disabled');
dom.cl.add(qs$(inspector, '.permatoolbar .commit'), 'disabled');
};

View File

@ -1846,6 +1846,48 @@ vAPI.messaging.listen({
/******************************************************************************/
/******************************************************************************/
// Channel:
// domInspectorContent
// unprivileged
{
// >>>>> start of local scope
const onMessage = (request, sender, callback) => {
// Async
switch ( request.what ) {
default:
break;
}
// Sync
let response;
switch ( request.what ) {
case 'getInspectorArgs':
response = {
inspectorURL: vAPI.getURL(
`/web_accessible_resources/dom-inspector.html?secret=${vAPI.warSecret.short()}`
),
};
break;
default:
return vAPI.messaging.UNHANDLED;
}
callback(response);
};
vAPI.messaging.listen({
name: 'domInspectorContent',
listener: onMessage,
privileged: false,
});
// <<<<< end of local scope
}
/******************************************************************************/
/******************************************************************************/
// Channel:
// documentBlocked
// privileged

View File

@ -1,7 +1,7 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2015-2018 Raymond Hill
Copyright (C) 2015-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
@ -24,30 +24,21 @@
/******************************************************************************/
/******************************************************************************/
(( ) => {
(async ( ) => {
/******************************************************************************/
if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { return; }
/******************************************************************************/
var sessionId = vAPI.sessionId;
if ( document.querySelector('iframe.dom-inspector.' + sessionId) !== null ) {
return;
}
if ( typeof vAPI !== 'object' ) { return; }
if ( vAPI.domFilterer instanceof Object === false ) { return; }
if ( document.querySelector(`iframe[${vAPI.sessionId}]`) !== null ) { return; }
/******************************************************************************/
/******************************************************************************/
let loggerConnectionId;
// Highlighter-related
let svgRoot = null;
let pickerRoot = null;
let inspectorRoot = null;
let nodeToIdMap = new WeakMap(); // No need to iterate
const nodeToIdMap = new WeakMap(); // No need to iterate
let blueNodes = [];
const roRedNodes = new Map(); // node => current cosmetic filter
@ -59,7 +50,11 @@ const reHasCSSCombinators = /[ >+~]/;
/******************************************************************************/
const domLayout = (function() {
//const getNodeId = node => nodeToIdMap.get(node) || 0;
/******************************************************************************/
const domLayout = (( ) => {
const skipTagNames = new Set([
'br', 'head', 'link', 'meta', 'script', 'style', 'title'
]);
@ -70,87 +65,81 @@ const domLayout = (function() {
[ 'object', 'data' ]
]);
var idGenerator = 0;
let idGenerator = 1;
// This will be used to uniquely identify nodes across process.
const newNodeId = function(node) {
var nid = 'n' + (idGenerator++).toString(36);
const newNodeId = node => {
const nid = `n${(idGenerator++).toString(36)}`;
nodeToIdMap.set(node, nid);
return nid;
};
const selectorFromNode = function(node) {
var str, attr, pos, sw, i;
var tag = node.localName;
var selector = CSS.escape(tag);
const selectorFromNode = node => {
const tag = node.localName;
let selector = CSS.escape(tag);
// Id
if ( typeof node.id === 'string' ) {
str = node.id.trim();
let str = node.id.trim();
if ( str !== '' ) {
selector += '#' + CSS.escape(str);
selector += `#${CSS.escape(str)}`;
}
}
// Class
var cl = node.classList;
const cl = node.classList;
if ( cl ) {
for ( i = 0; i < cl.length; i++ ) {
selector += '.' + CSS.escape(cl[i]);
for ( let i = 0; i < cl.length; i++ ) {
selector += `.${CSS.escape(cl[i])}`;
}
}
// Tag-specific attributes
attr = resourceAttrNames.get(tag);
const attr = resourceAttrNames.get(tag);
if ( attr !== undefined ) {
str = node.getAttribute(attr) || '';
let str = node.getAttribute(attr) || '';
str = str.trim();
if ( str.startsWith('data:') ) {
pos = 5;
} else {
pos = str.search(/[#?]/);
}
const pos = str.startsWith('data:') ? 5 : str.search(/[#?]/);
let sw = '';
if ( pos !== -1 ) {
str = str.slice(0, pos);
sw = '^';
} else {
sw = '';
}
if ( str !== '' ) {
selector += '[' + attr + sw + '="' + CSS.escape(str, true) + '"]';
selector += `[${attr}${sw}="${CSS.escape(str, true)}"]`;
}
}
return selector;
};
const DomRoot = function() {
function DomRoot() {
this.nid = newNodeId(document.body);
this.lvl = 0;
this.sel = 'body';
this.cnt = 0;
this.filter = roRedNodes.get(document.body);
};
}
const DomNode = function(node, level) {
function DomNode(node, level) {
this.nid = newNodeId(node);
this.lvl = level;
this.sel = selectorFromNode(node);
this.cnt = 0;
this.filter = roRedNodes.get(node);
};
}
const domNodeFactory = function(level, node) {
const domNodeFactory = (level, node) => {
const localName = node.localName;
if ( skipTagNames.has(localName) ) { return null; }
// skip uBlock's own nodes
if ( node.classList.contains(sessionId) ) { return null; }
if ( node === inspectorRoot ) { return null; }
if ( level === 0 && localName === 'body' ) {
return new DomRoot();
}
return new DomNode(node, level);
};
// Collect layout data.
// Collect layout data
const getLayoutData = function() {
const getLayoutData = ( ) => {
const layout = [];
const stack = [];
let lvl = 0;
@ -188,14 +177,14 @@ const domLayout = (function() {
// Descendant count for each node.
const patchLayoutData = function(layout) {
var stack = [], ptr;
var lvl = 0;
var domNode, cnt;
var i = layout.length;
const patchLayoutData = layout => {
const stack = [];
let ptr;
let lvl = 0;
let i = layout.length;
while ( i-- ) {
domNode = layout[i];
const domNode = layout[i];
if ( domNode.lvl === lvl ) {
stack[ptr] += 1;
continue;
@ -210,7 +199,7 @@ const domLayout = (function() {
continue;
}
// domNode.lvl < lvl
cnt = stack.pop();
const cnt = stack.pop();
domNode.cnt = cnt;
lvl -= 1;
ptr = lvl - 1;
@ -221,13 +210,13 @@ const domLayout = (function() {
// Track and report mutations of the DOM
var mutationObserver = null;
var mutationTimer;
var addedNodelists = [];
var removedNodelist = [];
let mutationObserver = null;
let mutationTimer;
let addedNodelists = [];
let removedNodelist = [];
const previousElementSiblingId = function(node) {
var sibling = node;
const previousElementSiblingId = node => {
let sibling = node;
for (;;) {
sibling = sibling.previousElementSibling;
if ( sibling === null ) { return null; }
@ -236,11 +225,10 @@ const domLayout = (function() {
}
};
const journalFromBranch = function(root, newNodes, newNodeToIdMap) {
var domNode;
var node = root.firstElementChild;
const journalFromBranch = (root, newNodes, newNodeToIdMap) => {
let node = root.firstElementChild;
while ( node !== null ) {
domNode = domNodeFactory(undefined, node);
const domNode = domNodeFactory(undefined, node);
if ( domNode !== null ) {
newNodeToIdMap.set(domNode.nid, domNode);
newNodes.push(node);
@ -267,22 +255,21 @@ const domLayout = (function() {
}
};
const journalFromMutations = function() {
var nodelist, node, domNode, nid;
const journalFromMutations = ( ) => {
mutationTimer = undefined;
// This is used to temporarily hold all added nodes, before resolving
// their node id and relative position.
var newNodes = [];
var journalEntries = [];
var newNodeToIdMap = new Map();
const newNodes = [];
const journalEntries = [];
const newNodeToIdMap = new Map();
for ( nodelist of addedNodelists ) {
for ( node of nodelist ) {
for ( const nodelist of addedNodelists ) {
for ( const node of nodelist ) {
if ( node.nodeType !== 1 ) { continue; }
if ( node.parentElement === null ) { continue; }
cosmeticFilterMapper.incremental(node);
domNode = domNodeFactory(undefined, node);
const domNode = domNodeFactory(undefined, node);
if ( domNode !== null ) {
newNodeToIdMap.set(domNode.nid, domNode);
newNodes.push(node);
@ -291,19 +278,16 @@ const domLayout = (function() {
}
}
addedNodelists = [];
for ( nodelist of removedNodelist ) {
for ( node of nodelist ) {
for ( const nodelist of removedNodelist ) {
for ( const node of nodelist ) {
if ( node.nodeType !== 1 ) { continue; }
nid = nodeToIdMap.get(node);
const nid = nodeToIdMap.get(node);
if ( nid === undefined ) { continue; }
journalEntries.push({
what: -1,
nid: nid
});
journalEntries.push({ what: -1, nid });
}
}
removedNodelist = [];
for ( node of newNodes ) {
for ( const node of newNodes ) {
journalEntries.push({
what: 1,
nid: nodeToIdMap.get(node),
@ -314,7 +298,7 @@ const domLayout = (function() {
if ( journalEntries.length === 0 ) { return; }
vAPI.MessagingConnection.sendTo(loggerConnectionId, {
inspectorFramePort.postMessage({
what: 'domLayoutIncremental',
url: window.location.href,
hostname: window.location.hostname,
@ -323,8 +307,8 @@ const domLayout = (function() {
});
};
const onMutationObserved = function(mutationRecords) {
for ( var record of mutationRecords ) {
const onMutationObserved = mutationRecords => {
for ( const record of mutationRecords ) {
if ( record.addedNodes.length !== 0 ) {
addedNodelists.push(record.addedNodes);
}
@ -339,7 +323,7 @@ const domLayout = (function() {
// API
const getLayout = function() {
const getLayout = ( ) => {
cosmeticFilterMapper.reset();
mutationObserver = new MutationObserver(onMutationObserved);
mutationObserver.observe(document.body, {
@ -355,11 +339,11 @@ const domLayout = (function() {
};
};
const reset = function() {
const reset = ( ) => {
shutdown();
};
const shutdown = function() {
const shutdown = ( ) => {
if ( mutationTimer !== undefined ) {
clearTimeout(mutationTimer);
mutationTimer = undefined;
@ -370,35 +354,20 @@ const domLayout = (function() {
}
addedNodelists = [];
removedNodelist = [];
nodeToIdMap = new WeakMap();
};
return {
get: getLayout,
reset: reset,
shutdown: shutdown
reset,
shutdown,
};
})();
// https://www.youtube.com/watch?v=qo8zKhd4Cf0
/******************************************************************************/
/******************************************************************************/
// For browsers not supporting `:scope`, it's not the end of the world: the
// suggested CSS selectors may just end up being more verbose.
let cssScope = ':scope > ';
try {
document.querySelector(':scope *');
} catch (e) {
cssScope = '';
}
/******************************************************************************/
const cosmeticFilterMapper = (function() {
const nodesFromStyleTag = function(rootNode) {
const cosmeticFilterMapper = (( ) => {
const nodesFromStyleTag = rootNode => {
const filterMap = roRedNodes;
const details = vAPI.domFilterer.getAllSelectors();
@ -434,25 +403,25 @@ const cosmeticFilterMapper = (function() {
}
};
const incremental = function(rootNode) {
const incremental = rootNode => {
nodesFromStyleTag(rootNode);
};
const reset = function() {
const reset = ( ) => {
roRedNodes.clear();
if ( document.documentElement !== null ) {
incremental(document.documentElement);
}
};
const shutdown = function() {
const shutdown = ( ) => {
vAPI.domFilterer.toggle(true);
};
return {
incremental: incremental,
reset: reset,
shutdown: shutdown
incremental,
reset,
shutdown,
};
})();
@ -475,21 +444,18 @@ const elementsFromSelector = function(selector, context) {
};
const elementsFromSpecialSelector = function(selector) {
var out = [], i;
var matches = /^(.+?):has\((.+?)\)$/.exec(selector);
const out = [];
let matches = /^(.+?):has\((.+?)\)$/.exec(selector);
if ( matches !== null ) {
var nodes;
let nodes;
try {
nodes = document.querySelectorAll(matches[1]);
} catch(ex) {
nodes = [];
}
i = nodes.length;
while ( i-- ) {
var node = nodes[i];
if ( node.querySelector(matches[2]) !== null ) {
out.push(node);
}
for ( const node of nodes ) {
if ( node.querySelector(matches[2]) === null ) { continue; }
out.push(node);
}
return out;
}
@ -503,7 +469,7 @@ const elementsFromSpecialSelector = function(selector) {
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null
);
i = xpr.snapshotLength;
let i = xpr.snapshotLength;
while ( i-- ) {
out.push(xpr.snapshotItem(i));
}
@ -512,128 +478,114 @@ const elementsFromSpecialSelector = function(selector) {
/******************************************************************************/
const getSvgRootChildren = function() {
if ( svgRoot.children ) {
return svgRoot.children;
} else {
const childNodes = Array.prototype.slice.apply(svgRoot.childNodes);
return childNodes.filter(function(node) {
return node.nodeType === Node.ELEMENT_NODE;
});
}
};
const highlightElements = ( ) => {
const paths = [];
const highlightElements = function() {
var islands;
var elem, rect, poly;
var xl, xr, yt, yb, w, h, ws;
var svgRootChildren = getSvgRootChildren();
islands = [];
for ( elem of rwRedNodes.keys() ) {
if ( elem === pickerRoot ) { continue; }
const path = [];
for ( const elem of rwRedNodes.keys() ) {
if ( elem === inspectorRoot ) { continue; }
if ( rwGreenNodes.has(elem) ) { continue; }
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
rect = elem.getBoundingClientRect();
xl = rect.left;
xr = rect.right;
w = rect.width;
yt = rect.top;
yb = rect.bottom;
h = rect.height;
ws = w.toFixed(1);
poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
const rect = elem.getBoundingClientRect();
const xl = rect.left;
const w = rect.width;
const yt = rect.top;
const h = rect.height;
const ws = w.toFixed(1);
const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
'h' + ws +
'v' + h.toFixed(1) +
'h-' + ws +
'z';
islands.push(poly);
path.push(poly);
}
svgRootChildren[0].setAttribute('d', islands.join('') || 'M0 0');
paths.push(path.join('') || 'M0 0');
islands = [];
for ( elem of rwGreenNodes ) {
path.length = 0;
for ( const elem of rwGreenNodes ) {
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
rect = elem.getBoundingClientRect();
xl = rect.left;
xr = rect.right;
w = rect.width;
yt = rect.top;
yb = rect.bottom;
h = rect.height;
ws = w.toFixed(1);
poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
const rect = elem.getBoundingClientRect();
const xl = rect.left;
const w = rect.width;
const yt = rect.top;
const h = rect.height;
const ws = w.toFixed(1);
const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
'h' + ws +
'v' + h.toFixed(1) +
'h-' + ws +
'z';
islands.push(poly);
path.push(poly);
}
svgRootChildren[1].setAttribute('d', islands.join('') || 'M0 0');
paths.push(path.join('') || 'M0 0');
islands = [];
for ( elem of roRedNodes.keys() ) {
if ( elem === pickerRoot ) { continue; }
path.length = 0;
for ( const elem of roRedNodes.keys() ) {
if ( elem === inspectorRoot ) { continue; }
if ( rwGreenNodes.has(elem) ) { continue; }
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
rect = elem.getBoundingClientRect();
xl = rect.left;
xr = rect.right;
w = rect.width;
yt = rect.top;
yb = rect.bottom;
h = rect.height;
ws = w.toFixed(1);
poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
const rect = elem.getBoundingClientRect();
const xl = rect.left;
const w = rect.width;
const yt = rect.top;
const h = rect.height;
const ws = w.toFixed(1);
const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
'h' + ws +
'v' + h.toFixed(1) +
'h-' + ws +
'z';
islands.push(poly);
path.push(poly);
}
svgRootChildren[2].setAttribute('d', islands.join('') || 'M0 0');
paths.push(path.join('') || 'M0 0');
islands = [];
for ( elem of blueNodes ) {
if ( elem === pickerRoot ) { continue; }
path.length = 0;
for ( const elem of blueNodes ) {
if ( elem === inspectorRoot ) { continue; }
if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; }
rect = elem.getBoundingClientRect();
xl = rect.left;
xr = rect.right;
w = rect.width;
yt = rect.top;
yb = rect.bottom;
h = rect.height;
ws = w.toFixed(1);
poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
const rect = elem.getBoundingClientRect();
const xl = rect.left;
const w = rect.width;
const yt = rect.top;
const h = rect.height;
const ws = w.toFixed(1);
const poly = 'M' + xl.toFixed(1) + ' ' + yt.toFixed(1) +
'h' + ws +
'v' + h.toFixed(1) +
'h-' + ws +
'z';
islands.push(poly);
path.push(poly);
}
svgRootChildren[3].setAttribute('d', islands.join('') || 'M0 0');
paths.push(path.join('') || 'M0 0');
inspectorFramePort.postMessage({
what: 'svgPaths',
paths,
});
};
/******************************************************************************/
const onScrolled = (function() {
let buffered = false;
const timerHandler = function() {
buffered = false;
highlightElements();
};
return function() {
if ( buffered === false ) {
window.requestAnimationFrame(timerHandler);
buffered = true;
}
const onScrolled = (( ) => {
let timer;
return ( ) => {
if ( timer ) { return; }
timer = window.requestAnimationFrame(( ) => {
timer = undefined;
highlightElements();
});
};
})();
const onMouseOver = ( ) => {
if ( blueNodes.length === 0 ) { return; }
blueNodes = [];
highlightElements();
};
/******************************************************************************/
const selectNodes = function(selector, nid) {
const selectNodes = (selector, nid) => {
const nodes = elementsFromSelector(selector);
if ( nid === '' ) { return nodes; }
for ( const node of nodes ) {
@ -646,7 +598,7 @@ const selectNodes = function(selector, nid) {
/******************************************************************************/
const nodesFromFilter = function(selector) {
const nodesFromFilter = selector => {
const out = [];
for ( const entry of roRedNodes ) {
if ( entry[1] === selector ) {
@ -658,7 +610,7 @@ const nodesFromFilter = function(selector) {
/******************************************************************************/
const toggleExceptions = function(nodes, targetState) {
const toggleExceptions = (nodes, targetState) => {
for ( const node of nodes ) {
if ( targetState ) {
rwGreenNodes.add(node);
@ -668,7 +620,7 @@ const toggleExceptions = function(nodes, targetState) {
}
};
const toggleFilter = function(nodes, targetState) {
const toggleFilter = (nodes, targetState) => {
for ( const node of nodes ) {
if ( targetState ) {
rwRedNodes.delete(node);
@ -678,23 +630,28 @@ const toggleFilter = function(nodes, targetState) {
}
};
const resetToggledNodes = function() {
const resetToggledNodes = ( ) => {
rwGreenNodes.clear();
rwRedNodes.clear();
};
/******************************************************************************/
const start = function() {
const onReady = function(ev) {
if ( ev ) {
document.removeEventListener(ev.type, onReady);
}
vAPI.MessagingConnection.sendTo(loggerConnectionId, domLayout.get());
const start = ( ) => {
const onReady = ( ) => {
window.addEventListener('scroll', onScrolled, {
capture: true,
passive: true,
});
window.addEventListener('mouseover', onMouseOver, {
capture: true,
passive: true,
});
inspectorFramePort.postMessage(domLayout.get());
vAPI.domFilterer.toggle(false, highlightElements);
};
if ( document.readyState === 'loading' ) {
document.addEventListener('DOMContentLoaded', onReady);
document.addEventListener('DOMContentLoaded', onReady, { once: true });
} else {
onReady();
}
@ -702,34 +659,49 @@ const start = function() {
/******************************************************************************/
const shutdown = function() {
const shutdown = ( ) => {
cosmeticFilterMapper.shutdown();
domLayout.shutdown();
vAPI.MessagingConnection.disconnectFrom(loggerConnectionId);
window.removeEventListener('scroll', onScrolled, true);
pickerRoot.remove();
pickerRoot = svgRoot = null;
window.removeEventListener('scroll', onScrolled, {
capture: true,
passive: true,
});
window.removeEventListener('mouseover', onMouseOver, {
capture: true,
passive: true,
});
inspectorFramePort.close();
inspectorFramePort = undefined;
vAPI.userStylesheet.remove(inspectorCSS);
vAPI.userStylesheet.apply();
if ( inspectorRoot === null ) { return; }
inspectorRoot.remove();
inspectorRoot = null;
};
/******************************************************************************/
/******************************************************************************/
const onMessage = function(request) {
var response,
nodes;
const onMessage = request => {
switch ( request.what ) {
case 'startInspector':
start();
break;
case 'quitInspector':
shutdown();
break;
case 'commitFilters':
highlightElements();
break;
case 'domLayout':
response = domLayout.get();
domLayout.get();
highlightElements();
break;
case 'highlightMode':
//svgRoot.classList.toggle('invert', request.invert);
break;
case 'highlightOne':
@ -753,110 +725,47 @@ const onMessage = function(request) {
highlightElements();
break;
case 'toggleFilter':
nodes = selectNodes(request.selector, request.nid);
if ( nodes.length !== 0 ) { nodes[0].scrollIntoView(); }
case 'toggleFilter': {
const nodes = selectNodes(request.selector, request.nid);
if ( nodes.length !== 0 ) {
nodes[0].scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
}
toggleExceptions(nodesFromFilter(request.filter), request.target);
highlightElements();
break;
case 'toggleNodes':
nodes = selectNodes(request.selector, request.nid);
if ( nodes.length !== 0 ) { nodes[0].scrollIntoView(); }
}
case 'toggleNodes': {
const nodes = selectNodes(request.selector, request.nid);
if ( nodes.length !== 0 ) {
nodes[0].scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
}
toggleFilter(nodes, request.target);
highlightElements();
break;
}
default:
break;
}
return response;
};
/******************************************************************************/
// Install DOM inspector widget
let inspectorArgs = await vAPI.messaging.send('domInspectorContent', {
what: 'getInspectorArgs',
});
if ( typeof inspectorArgs !== 'object' ) { return; }
if ( inspectorArgs === null ) { return; }
const bootstrap = function(ev) {
if ( ev ) {
pickerRoot.removeEventListener(ev.type, bootstrap);
}
const pickerDoc = ev.target.contentDocument;
pickerDoc.documentElement.style.setProperty(
'color-scheme',
'dark light',
'important'
);
const style = pickerDoc.createElement('style');
style.textContent = [
'body {',
'background-color: transparent;',
'}',
'svg {',
'height: 100%;',
'left: 0;',
'position: fixed;',
'top: 0;',
'width: 100%;',
'}',
'svg > path:nth-of-type(1) {',
'fill: rgba(255,0,0,0.2);',
'stroke: #F00;',
'}',
'svg > path:nth-of-type(2) {',
'fill: rgba(0,255,0,0.2);',
'stroke: #0F0;',
'}',
'svg > path:nth-of-type(3) {',
'fill: rgba(255,0,0,0.2);',
'stroke: #F00;',
'}',
'svg > path:nth-of-type(4) {',
'fill: rgba(0,0,255,0.1);',
'stroke: #FFF;',
'stroke-width: 0.5px;',
'}',
''
].join('\n');
pickerDoc.body.appendChild(style);
svgRoot = pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'svg');
svgRoot.appendChild(pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'));
svgRoot.appendChild(pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'));
svgRoot.appendChild(pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'));
svgRoot.appendChild(pickerDoc.createElementNS('http://www.w3.org/2000/svg', 'path'));
pickerDoc.body.appendChild(svgRoot);
window.addEventListener('scroll', onScrolled, true);
// Dynamically add direct connection abilities so that we can establish
// a direct, fast messaging connection to the logger.
vAPI.messaging.extend().then(extended => {
if ( extended !== true ) { return; }
vAPI.MessagingConnection.connectTo('domInspector', 'loggerUI', msg => {
switch ( msg.what ) {
case 'connectionAccepted':
loggerConnectionId = msg.id;
start();
break;
case 'connectionBroken':
shutdown();
break;
case 'connectionMessage':
onMessage(msg.payload);
break;
}
});
});
};
pickerRoot = document.createElement('iframe');
pickerRoot.classList.add(sessionId);
pickerRoot.classList.add('dom-inspector');
pickerRoot.style.cssText = [
const inspectorCSSStyle = [
'background: transparent',
'border: 0',
'border-radius: 0',
@ -878,8 +787,43 @@ pickerRoot.style.cssText = [
''
].join(' !important;\n');
pickerRoot.addEventListener('load', ev => { bootstrap(ev); });
(document.documentElement || document).appendChild(pickerRoot);
const inspectorCSS = `
:root > [${vAPI.sessionId}] {
${inspectorCSSStyle}
}
:root > [${vAPI.sessionId}-loaded] {
visibility: visible !important;
}
`;
vAPI.userStylesheet.add(inspectorCSS);
vAPI.userStylesheet.apply();
inspectorRoot = document.createElement('iframe');
inspectorRoot.setAttribute(vAPI.sessionId, '');
document.documentElement.append(inspectorRoot);
let inspectorFramePort;
inspectorRoot.addEventListener('load', ( ) => {
const channel = new MessageChannel();
inspectorFramePort = channel.port1;
inspectorFramePort.onmessage = ev => {
const msg = ev.data || {};
onMessage(msg);
};
inspectorFramePort.onmessageerror = ( ) => {
shutdown();
};
inspectorRoot.setAttribute(`${vAPI.sessionId}-loaded`, '');
inspectorRoot.contentWindow.postMessage(
{ what: 'startInspector' },
inspectorArgs.inspectorURL,
[ channel.port2 ]
);
}, { once: true });
inspectorRoot.contentWindow.location = inspectorArgs.inspectorURL;
/******************************************************************************/

View File

@ -37,7 +37,6 @@ if ( typeof vAPI !== 'object' || vAPI === null ) {
/******************************************************************************/
const epickerId = vAPI.randomToken();
let epickerConnectionId;
let pickerRoot = document.querySelector(`[${vAPI.sessionId}]`);
if ( pickerRoot !== null ) { return; }
@ -144,7 +143,7 @@ const highlightElements = function(elems, force) {
);
}
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerFramePort.postMessage({
what: 'svgPaths',
ocean: `M0 0h${ow}v${oh}h-${ow}z`,
islands: islands.join(''),
@ -900,7 +899,7 @@ const onOptimizeCandidates = function(details) {
if ( r !== 0 ) { return r; }
return a.selector.length - b.selector.length;
});
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerFramePort.postMessage({
what: 'candidatesOptimized',
candidates: results.map(a => a.selector),
slot: details.slot,
@ -910,7 +909,7 @@ const onOptimizeCandidates = function(details) {
/******************************************************************************/
const showDialog = function(options) {
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerFramePort.postMessage({
what: 'showDialog',
url: self.location.href,
netFilters: netFilterCandidates,
@ -1141,16 +1140,13 @@ const quitPicker = function() {
self.removeEventListener('resize', onViewportChanged, { passive: true });
self.removeEventListener('keydown', onKeyPressed, true);
vAPI.shutdown.remove(quitPicker);
vAPI.MessagingConnection.disconnectFrom(epickerConnectionId);
vAPI.MessagingConnection.removeListener(onConnectionMessage);
pickerFramePort.close();
pickerFramePort = undefined;
vAPI.userStylesheet.remove(pickerCSS);
vAPI.userStylesheet.apply();
if ( pickerRoot === null ) { return; }
pickerRoot.remove();
pickerRoot = null;
self.focus();
};
@ -1176,7 +1172,7 @@ const onDialogMessage = function(msg) {
const resultset = filterToDOMInterface.queryAll(msg) || [];
highlightElements(resultset.map(a => a.elem), true);
if ( msg.filter === '!' ) { break; }
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
pickerFramePort.postMessage({
what: 'resultsetDetails',
count: resultset.length,
opt: resultset.length !== 0 ? resultset[0].opt : undefined,
@ -1215,23 +1211,6 @@ const onDialogMessage = function(msg) {
/******************************************************************************/
const onConnectionMessage = function(msg) {
if ( msg.from !== `epickerDialog-${epickerId}` ) { return; }
switch ( msg.what ) {
case 'connectionRequested':
epickerConnectionId = msg.id;
return true;
case 'connectionBroken':
quitPicker();
break;
case 'connectionMessage':
onDialogMessage(msg.payload);
break;
}
};
/******************************************************************************/
// epicker-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.
@ -1249,17 +1228,13 @@ const onConnectionMessage = function(msg) {
// of the iframe, and cannot interfere with its style properties. However the
// page can remove the iframe.
// We need extra messaging capabilities + fetch/process picker arguments.
// fetch/process picker arguments.
{
const results = await Promise.all([
vAPI.messaging.extend(),
vAPI.messaging.send('elementPicker', { what: 'elementPickerArguments' }),
]);
if ( results[0] !== true ) { return; }
pickerBootArgs = results[1];
if ( typeof pickerBootArgs !== 'object' || pickerBootArgs === null ) {
return;
}
pickerBootArgs = await vAPI.messaging.send('elementPicker', {
what: 'elementPickerArguments',
});
if ( typeof pickerBootArgs !== 'object' ) { return; }
if ( pickerBootArgs === null ) { return; }
// Restore net filter union data if origin is the same.
const eprom = pickerBootArgs.eprom || null;
if ( eprom !== null && eprom.lastNetFilterSession === lastNetFilterSession ) {
@ -1302,12 +1277,12 @@ const pickerCSSStyle = [
'width: 100%',
'z-index: 2147483647',
''
];
].join(' !important;\n');
const pickerCSS = `
:root > [${vAPI.sessionId}] {
${pickerCSSStyle.join(' !important;')}
${pickerCSSStyle}
}
:root > [${vAPI.sessionId}-loaded] {
visibility: visible !important;
@ -1326,7 +1301,7 @@ document.documentElement.append(pickerRoot);
vAPI.shutdown.add(quitPicker);
vAPI.MessagingConnection.addListener(onConnectionMessage);
let pickerFramePort;
{
const url = new URL(pickerBootArgs.pickerURL);
@ -1334,10 +1309,24 @@ vAPI.MessagingConnection.addListener(onConnectionMessage);
if ( pickerBootArgs.zap ) {
url.searchParams.set('zap', '1');
}
pickerRoot.addEventListener("load", function() {
pickerRoot.setAttribute(`${vAPI.sessionId}-loaded`, "");
});
pickerRoot.src = url.href;
pickerRoot.addEventListener('load', ( ) => {
const channel = new MessageChannel();
pickerFramePort = channel.port1;
pickerFramePort.onmessage = ev => {
const msg = ev.data || {};
onDialogMessage(msg);
};
pickerFramePort.onmessageerror = ( ) => {
quitPicker();
};
pickerRoot.setAttribute(`${vAPI.sessionId}-loaded`, '');
pickerRoot.contentWindow.postMessage(
{ what: 'epickerStart' },
url.href,
[ channel.port2 ]
);
}, { once: true });
pickerRoot.contentWindow.location = url.href;
}
/******************************************************************************/

View File

@ -222,7 +222,6 @@
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/theme.js" type="module"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/logger-ui.js" type="module"></script>

View File

@ -121,7 +121,6 @@
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/theme.js" type="module"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js" type="module"></script>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html id="ublock0-inspector">
<head>
<meta charset="utf-8">
<title>uBlock Origin Inspector</title>
<link rel="stylesheet" href="../css/dom-inspector.css">
</head>
<body>
<svg>
<path d></path>
<path d></path>
<path d></path>
<path d></path>
</svg>
<script src="../js/vapi.js"></script>
<script src="../js/vapi-common.js"></script>
<script src="../js/vapi-client.js"></script>
<script src="../js/dom-inspector.js" type="module"></script>
</body>
</html>

View File

@ -67,7 +67,6 @@
<script src="../js/vapi.js"></script>
<script src="../js/vapi-common.js"></script>
<script src="../js/vapi-client.js"></script>
<script src="../js/vapi-client-extra.js"></script>
<script src="../js/theme.js" type="module"></script>
<script src="../js/i18n.js" type="module"></script>
<script src="../js/epicker-ui.js" type="module"></script>