diff --git a/src/js/dom-inspector.js b/src/js/dom-inspector.js index fc4b9ef3a..a0d334b76 100644 --- a/src/js/dom-inspector.js +++ b/src/js/dom-inspector.js @@ -25,22 +25,19 @@ /******************************************************************************/ const svgRoot = document.querySelector('svg'); +let inspectorContentPort; -const quit = ( ) => { - inspectorContentPort.postMessage({ what: 'quitInspector' }); +const shutdown = ( ) => { 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) => { +const contentInspectorChannel = ev => { + const msg = ev.data || {}; switch ( msg.what ) { case 'quitInspector': { - quit(); + shutdown(); break; } case 'svgPaths': { @@ -52,41 +49,20 @@ const onMessage = (msg, fromLogger) => { 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.onmessage = contentInspectorChannel; + inspectorContentPort.onmessageerror = shutdown; inspectorContentPort.postMessage({ what: 'startInspector' }); }, { once: true }); + +/******************************************************************************/ diff --git a/src/js/logger-ui-inspector.js b/src/js/logger-ui-inspector.js index c48d50c6b..d8808ce94 100644 --- a/src/js/logger-ui-inspector.js +++ b/src/js/logger-ui-inspector.js @@ -42,21 +42,97 @@ let inspectedURL = ''; let inspectedHostname = ''; let uidGenerator = 1; -/******************************************************************************/ +/******************************************************************************* + * + * How it works: + * + * 1. The logger/inspector is enabled from the logger window + * + * 2. The inspector content script is injected in the root frame of the tab + * currently selected in the logger + * + * 3. The inspector content script asks the logger/inspector to establish + * a two-way communication channel + * + * 3. The inspector content script embed an inspector frame in the document + * being inspected and waits for the inspector frame to be fully loaded + * + * 4. The inspector content script sends a messaging port object to the + * embedded inspector frame for a two-way communication channel between + * the inspector frame and the inspector content script + * + * 5. The inspector content script sends dom information to the + * logger/inspector + * + * */ -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 contentInspectorChannel = (( ) => { + let bcChannel; + let toContentPort; + + const start = ( ) => { + bcChannel = new globalThis.BroadcastChannel('contentInspectorChannel'); + bcChannel.onmessage = ev => { + const msg = ev.data || {}; + connect(msg.tabId, msg.frameId); + }; + browser.webNavigation.onDOMContentLoaded.addListener(onContentLoaded); + }; + + const shutdown = ( ) => { + browser.webNavigation.onDOMContentLoaded.removeListener(onContentLoaded); + disconnect(); + bcChannel.close(); + bcChannel.onmessage = null; + bcChannel = undefined; + }; + + const connect = (tabId, frameId) => { + disconnect(); + try { + toContentPort = browser.tabs.connect(tabId, { frameId }); + toContentPort.onMessage.addListener(onContentMessage); + toContentPort.onDisconnect.addListener(onContentDisconnect); + } catch(_) { + } + }; + + const disconnect = ( ) => { + if ( toContentPort === undefined ) { return; } + toContentPort.onMessage.removeListener(onContentMessage); + toContentPort.onDisconnect.removeListener(onContentDisconnect); + toContentPort.disconnect(); + toContentPort = undefined; + }; + + const send = msg => { + if ( toContentPort === undefined ) { return; } + toContentPort.postMessage(msg); + }; + + const onContentMessage = msg => { + if ( msg.what === 'domLayoutFull' ) { + inspectedURL = msg.url; + inspectedHostname = msg.hostname; + renderDOMFull(msg); + } else if ( msg.what === 'domLayoutIncremental' ) { + renderDOMIncremental(msg); + } + }; + + const onContentDisconnect = ( ) => { + disconnect(); + }; + + const onContentLoaded = details => { + if ( details.tabId !== inspectedTabId ) { return; } + if ( details.frameId !== 0 ) { return; } + disconnect(); + injectInspector(); + }; + + return { start, disconnect, send, shutdown }; +})(); /******************************************************************************/ @@ -345,7 +421,7 @@ const startDialog = (( ) => { }; const showCommitted = function() { - inspectorFramePort.postMessage({ + contentInspectorChannel.send({ what: 'showCommitted', hide: hideSelectors.join(',\n'), unhide: unhideSelectors.join(',\n') @@ -353,7 +429,7 @@ const startDialog = (( ) => { }; const showInteractive = function() { - inspectorFramePort.postMessage({ + contentInspectorChannel.send({ what: 'showInteractive', hide: hideSelectors.join(',\n'), unhide: unhideSelectors.join(',\n') @@ -432,7 +508,7 @@ const onClicked = ev => { // Toggle cosmetic filter if ( dom.cl.has(target, 'filter') ) { - inspectorFramePort.postMessage({ + contentInspectorChannel.send({ what: 'toggleFilter', original: false, target: dom.cl.toggle(target, 'off'), @@ -448,7 +524,7 @@ const onClicked = ev => { } // Toggle node else { - inspectorFramePort.postMessage({ + contentInspectorChannel.send({ what: 'toggleNodes', original: true, target: dom.cl.toggle(target, 'off') === false, @@ -468,7 +544,7 @@ const onMouseOver = (( ) => { let mouseoverTarget = null; const mouseoverTimer = vAPI.defer.create(( ) => { - inspectorFramePort.postMessage({ + contentInspectorChannel.send({ what: 'highlightOne', selector: selectorFromNode(mouseoverTarget), nid: nidFromNode(mouseoverTarget), @@ -517,7 +593,7 @@ const injectInspector = (( ) => { /******************************************************************************/ const shutdownInspector = ( ) => { - inspectorFramePort.postMessage({ what: 'quitInspector' }); + contentInspectorChannel.disconnect(); logger.removeAllChildren(domTree); dom.cl.remove(inspector, 'vExpanded'); inspectedTabId = 0; @@ -537,14 +613,6 @@ const onTabIdChanged = ( ) => { /******************************************************************************/ -const onDOMContentLoaded = details => { - if ( details.tabId !== inspectedTabId ) { return; } - if ( details.frameId !== 0 ) { return; } - injectInspector(); -}; - -/******************************************************************************/ - const toggleVCompactView = ( ) => { const state = dom.cl.toggle(inspector, 'vExpanded'); const branches = qsa$('#domInspector li.branch'); @@ -561,7 +629,7 @@ const toggleHCompactView = ( ) => { const revert = ( ) => { dom.cl.remove('#domTree .off', 'off'); - inspectorFramePort.postMessage({ what: 'resetToggledNodes' }); + contentInspectorChannel.send({ what: 'resetToggledNodes' }); dom.cl.add(qs$(inspector, '.permatoolbar .revert'), 'disabled'); dom.cl.add(qs$(inspector, '.permatoolbar .commit'), 'disabled'); }; @@ -578,7 +646,7 @@ const toggleOn = ( ) => { dom.on('#domInspector .hCompactToggler', 'click', toggleHCompactView); dom.on('#domInspector .permatoolbar .revert', 'click', revert); dom.on('#domInspector .permatoolbar .commit', 'click', startDialog); - browser.webNavigation.onDOMContentLoaded.addListener(onDOMContentLoaded); + contentInspectorChannel.start(); injectInspector(); }; @@ -596,7 +664,7 @@ const toggleOff = ( ) => { dom.off('#domInspector .hCompactToggler', 'click', toggleHCompactView); dom.off('#domInspector .permatoolbar .revert', 'click', revert); dom.off('#domInspector .permatoolbar .commit', 'click', startDialog); - browser.webNavigation.onDOMContentLoaded.removeListener(onDOMContentLoaded); + contentInspectorChannel.shutdown(); inspectedTabId = 0; }; diff --git a/src/js/messaging.js b/src/js/messaging.js index 958cae300..2419c6c98 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1860,6 +1860,12 @@ const onMessage = (request, sender, callback) => { let response; switch ( request.what ) { case 'getInspectorArgs': + const bc = new globalThis.BroadcastChannel('contentInspectorChannel'); + bc.postMessage({ + what: 'contentInspectorChannel', + tabId: sender.tabId || 0, + frameId: sender.frameId || 0, + }); response = { inspectorURL: vAPI.getURL( `/web_accessible_resources/dom-inspector.html?secret=${vAPI.warSecret.short()}` diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index da51a8302..20882016b 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -19,6 +19,8 @@ Home: https://github.com/gorhill/uBlock */ +/* globals browser */ + 'use strict'; /******************************************************************************/ @@ -35,9 +37,6 @@ if ( document.querySelector(`iframe[${vAPI.sessionId}]`) !== null ) { return; } /******************************************************************************/ /******************************************************************************/ -// Highlighter-related -let inspectorRoot = null; - const nodeToIdMap = new WeakMap(); // No need to iterate let blueNodes = []; @@ -130,7 +129,7 @@ const domLayout = (( ) => { const localName = node.localName; if ( skipTagNames.has(localName) ) { return null; } // skip uBlock's own nodes - if ( node === inspectorRoot ) { return null; } + if ( node === inspectorFrame ) { return null; } if ( level === 0 && localName === 'body' ) { return new DomRoot(); } @@ -298,7 +297,7 @@ const domLayout = (( ) => { if ( journalEntries.length === 0 ) { return; } - inspectorFramePort.postMessage({ + contentInspectorChannel.toLogger({ what: 'domLayoutIncremental', url: window.location.href, hostname: window.location.hostname, @@ -483,7 +482,7 @@ const highlightElements = ( ) => { const path = []; for ( const elem of rwRedNodes.keys() ) { - if ( elem === inspectorRoot ) { continue; } + if ( elem === inspectorFrame ) { continue; } if ( rwGreenNodes.has(elem) ) { continue; } if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; } const rect = elem.getBoundingClientRect(); @@ -521,7 +520,7 @@ const highlightElements = ( ) => { path.length = 0; for ( const elem of roRedNodes.keys() ) { - if ( elem === inspectorRoot ) { continue; } + if ( elem === inspectorFrame ) { continue; } if ( rwGreenNodes.has(elem) ) { continue; } if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; } const rect = elem.getBoundingClientRect(); @@ -541,7 +540,7 @@ const highlightElements = ( ) => { path.length = 0; for ( const elem of blueNodes ) { - if ( elem === inspectorRoot ) { continue; } + if ( elem === inspectorFrame ) { continue; } if ( typeof elem.getBoundingClientRect !== 'function' ) { continue; } const rect = elem.getBoundingClientRect(); const xl = rect.left; @@ -558,7 +557,7 @@ const highlightElements = ( ) => { } paths.push(path.join('') || 'M0 0'); - inspectorFramePort.postMessage({ + contentInspectorChannel.toFrame({ what: 'svgPaths', paths, }); @@ -637,7 +636,7 @@ const resetToggledNodes = ( ) => { /******************************************************************************/ -const start = ( ) => { +const startInspector = ( ) => { const onReady = ( ) => { window.addEventListener('scroll', onScrolled, { capture: true, @@ -647,7 +646,7 @@ const start = ( ) => { capture: true, passive: true, }); - inspectorFramePort.postMessage(domLayout.get()); + contentInspectorChannel.toLogger(domLayout.get()); vAPI.domFilterer.toggle(false, highlightElements); }; if ( document.readyState === 'loading' ) { @@ -659,7 +658,7 @@ const start = ( ) => { /******************************************************************************/ -const shutdown = ( ) => { +const shutdownInspector = ( ) => { cosmeticFilterMapper.shutdown(); domLayout.shutdown(); window.removeEventListener('scroll', onScrolled, { @@ -670,13 +669,12 @@ const shutdown = ( ) => { capture: true, passive: true, }); - inspectorFramePort.close(); - inspectorFramePort = undefined; + contentInspectorChannel.shutdown(); vAPI.userStylesheet.remove(inspectorCSS); vAPI.userStylesheet.apply(); - if ( inspectorRoot === null ) { return; } - inspectorRoot.remove(); - inspectorRoot = null; + if ( inspectorFrame === null ) { return; } + inspectorFrame.remove(); + inspectorFrame = null; }; /******************************************************************************/ @@ -685,11 +683,11 @@ const shutdown = ( ) => { const onMessage = request => { switch ( request.what ) { case 'startInspector': - start(); + startInspector(); break; case 'quitInspector': - shutdown(); + shutdownInspector(); break; case 'commitFilters': @@ -756,15 +754,97 @@ const onMessage = request => { } }; -/******************************************************************************/ +/******************************************************************************* + * + * Establish two-way communication with logger/inspector window and + * inspector frame + * + * */ + +const contentInspectorChannel = (( ) => { + let toLoggerPort; + let toFramePort; + + const toLogger = msg => { + if ( toLoggerPort === undefined ) { return; } + try { + toLoggerPort.postMessage(msg); + } catch(_) { + shutdownInspector(); + } + }; + + const onLoggerMessage = msg => { + onMessage(msg); + }; + + const onLoggerDisconnect = ( ) => { + shutdownInspector(); + }; + + const onLoggerConnect = port => { + browser.runtime.onConnect.removeListener(onLoggerConnect); + toLoggerPort = port; + port.onMessage.addListener(onLoggerMessage); + port.onDisconnect.addListener(onLoggerDisconnect); + }; + + const toFrame = msg => { + if ( toFramePort === undefined ) { return; } + toFramePort.postMessage(msg); + }; + + const shutdown = ( ) => { + if ( toFramePort !== undefined ) { + toFrame({ what: 'quitInspector' }); + toFramePort.onmessage = null; + toFramePort.close(); + toFramePort = undefined; + } + if ( toLoggerPort !== undefined ) { + toLoggerPort.onMessage.removeListener(onLoggerMessage); + toLoggerPort.onDisconnect.removeListener(onLoggerDisconnect); + toLoggerPort.disconnect(); + toLoggerPort = undefined; + } + browser.runtime.onConnect.removeListener(onLoggerConnect); + }; + + const start = async ( ) => { + browser.runtime.onConnect.addListener(onLoggerConnect); + const inspectorArgs = await vAPI.messaging.send('domInspectorContent', { + what: 'getInspectorArgs', + }); + if ( typeof inspectorArgs !== 'object' ) { return; } + if ( inspectorArgs === null ) { return; } + return new Promise(resolve => { + const iframe = document.createElement('iframe'); + iframe.setAttribute(vAPI.sessionId, ''); + document.documentElement.append(iframe); + iframe.addEventListener('load', ( ) => { + iframe.setAttribute(`${vAPI.sessionId}-loaded`, ''); + const channel = new MessageChannel(); + toFramePort = channel.port1; + toFramePort.onmessage = ev => { + const msg = ev.data || {}; + if ( msg.what !== 'startInspector' ) { return; } + resolve(iframe); + }; + iframe.contentWindow.postMessage( + { what: 'startInspector' }, + inspectorArgs.inspectorURL, + [ channel.port2 ] + ); + }, { once: true }); + iframe.contentWindow.location = inspectorArgs.inspectorURL; + }); + }; + + return { start, toLogger, toFrame, shutdown }; +})(); + // Install DOM inspector widget -let inspectorArgs = await vAPI.messaging.send('domInspectorContent', { - what: 'getInspectorArgs', -}); -if ( typeof inspectorArgs !== 'object' ) { return; } -if ( inspectorArgs === null ) { return; } - const inspectorCSSStyle = [ 'background: transparent', 'border: 0', @@ -799,31 +879,12 @@ const inspectorCSS = ` vAPI.userStylesheet.add(inspectorCSS); vAPI.userStylesheet.apply(); -inspectorRoot = document.createElement('iframe'); -inspectorRoot.setAttribute(vAPI.sessionId, ''); -document.documentElement.append(inspectorRoot); +let inspectorFrame = await contentInspectorChannel.start(); +if ( inspectorFrame instanceof HTMLIFrameElement === false ) { + return shutdownInspector(); +} -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; +startInspector(); /******************************************************************************/