From ee83a4304a85a7744973b4ff61aa7c2405e7b885 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 3 Dec 2023 16:21:32 -0500 Subject: [PATCH] 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. --- platform/common/vapi-background.js | 60 -- platform/common/vapi-client-extra.js | 264 -------- platform/common/vapi-client.js | 33 +- src/1p-filters.html | 1 - src/3p-filters.html | 1 - src/css/dom-inspector.css | 38 ++ src/js/dom-inspector.js | 92 +++ src/js/epicker-ui.js | 92 ++- src/js/logger-ui-inspector.js | 156 ++--- src/js/messaging.js | 42 ++ src/js/scriptlets/dom-inspector.js | 578 ++++++++---------- src/js/scriptlets/epicker.js | 77 +-- src/logger-ui.html | 1 - src/support.html | 1 - .../dom-inspector.html | 24 + src/web_accessible_resources/epicker-ui.html | 1 - 16 files changed, 586 insertions(+), 875 deletions(-) delete mode 100644 platform/common/vapi-client-extra.js create mode 100644 src/css/dom-inspector.css create mode 100644 src/js/dom-inspector.js create mode 100644 src/web_accessible_resources/dom-inspector.html diff --git a/platform/common/vapi-background.js b/platform/common/vapi-background.js index 297e69b2b6d26..72f87ed25bb5e 100644 --- a/platform/common/vapi-background.js +++ b/platform/common/vapi-background.js @@ -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 || []; diff --git a/platform/common/vapi-client-extra.js b/platform/common/vapi-client-extra.js deleted file mode 100644 index a24c1e9c1237c..0000000000000 --- a/platform/common/vapi-client-extra.js +++ /dev/null @@ -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; diff --git a/platform/common/vapi-client.js b/platform/common/vapi-client.js index 56686b1ff56cd..8d8cd12fc10ce 100644 --- a/platform/common/vapi-client.js +++ b/platform/common/vapi-client.js @@ -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(( ) => { diff --git a/src/1p-filters.html b/src/1p-filters.html index 110a59895bb42..bafa9922c557f 100644 --- a/src/1p-filters.html +++ b/src/1p-filters.html @@ -53,7 +53,6 @@ - diff --git a/src/3p-filters.html b/src/3p-filters.html index 49bd33ff75bd4..b51e1f1b03eb0 100644 --- a/src/3p-filters.html +++ b/src/3p-filters.html @@ -108,7 +108,6 @@ - diff --git a/src/css/dom-inspector.css b/src/css/dom-inspector.css new file mode 100644 index 0000000000000..96f1d40dea4d8 --- /dev/null +++ b/src/css/dom-inspector.css @@ -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; +} diff --git a/src/js/dom-inspector.js b/src/js/dom-inspector.js new file mode 100644 index 0000000000000..a46c991f8f9eb --- /dev/null +++ b/src/js/dom-inspector.js @@ -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 }); diff --git a/src/js/epicker-ui.js b/src/js/epicker-ui.js index ccddf95fc950a..0a5ceba528500 100644 --- a/src/js/epicker-ui.js +++ b/src/js/epicker-ui.js @@ -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 + +let pickerContentPort; -vAPI.MessagingConnection.connectTo( - `epickerDialog-${epickerId}`, - `epicker-${epickerId}`, - onConnectionMessage -); +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; diff --git a/src/js/logger-ui-inspector.js b/src/js/logger-ui-inspector.js index c423c3693cbd3..8eedc3fffa2cf 100644 --- a/src/js/logger-ui-inspector.js +++ b/src/js/logger-ui-inspector.js @@ -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'); }; diff --git a/src/js/messaging.js b/src/js/messaging.js index bac70d8106b21..faa340d82ff07 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -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 diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index 2f0abf04c7c8e..aa8205eb51012 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -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 = function() { - var islands; - var elem, rect, poly; - var xl, xr, yt, yb, w, h, ws; - var svgRootChildren = getSvgRootChildren(); +const highlightElements = ( ) => { + const paths = []; - 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; /******************************************************************************/ diff --git a/src/js/scriptlets/epicker.js b/src/js/scriptlets/epicker.js index 402ac7599b0b5..277c80a1f36ff 100644 --- a/src/js/scriptlets/epicker.js +++ b/src/js/scriptlets/epicker.js @@ -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; } /******************************************************************************/ diff --git a/src/logger-ui.html b/src/logger-ui.html index e8d625be1dcce..389856d85c625 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -222,7 +222,6 @@ - diff --git a/src/support.html b/src/support.html index 79279be17cb0d..12329423fecf2 100644 --- a/src/support.html +++ b/src/support.html @@ -121,7 +121,6 @@

- diff --git a/src/web_accessible_resources/dom-inspector.html b/src/web_accessible_resources/dom-inspector.html new file mode 100644 index 0000000000000..6afdbfaa9ac69 --- /dev/null +++ b/src/web_accessible_resources/dom-inspector.html @@ -0,0 +1,24 @@ + + + + + +uBlock Origin Inspector + + + + + + + + + + + + + + + + + + diff --git a/src/web_accessible_resources/epicker-ui.html b/src/web_accessible_resources/epicker-ui.html index 09b22b5ffa067..bd92f50115e97 100644 --- a/src/web_accessible_resources/epicker-ui.html +++ b/src/web_accessible_resources/epicker-ui.html @@ -67,7 +67,6 @@ -