From 87d0e456f1997cddb75168ee8dff6c8afcae1636 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Thu, 19 Sep 2019 08:31:38 -0400 Subject: [PATCH] Simplify client messaging code Little-used code from vapi-client.js has been moved to vapi-client-extra.js. Given that vapi-client.js is injected in all web pages, this means less dead code being injected in all pages. Swathes of code in vapi-client.js was used only in a few very specific cases, such as when the logger's DOM inspector is opened or when the "Filter lists" pane in the dashboard is opened -- and thus to avoid that little used code to be loaded in every web page unconditionally, it has been moved to its own separate file, vapi-client.extra.js. vapi-client-extra.js is loaded declaratively or programmatically only where needed. --- platform/chromium/vapi-background.js | 20 +- platform/chromium/vapi-client-extra.js | 308 +++++++++++++++++++++++++ platform/chromium/vapi-client.js | 281 +++------------------- src/3p-filters.html | 1 + src/js/3p-filters.js | 9 +- src/js/cosmetic-filtering.js | 5 +- src/js/logger-ui-inspector.js | 72 +++--- src/js/scriptlets/cosmetic-logger.js | 26 ++- src/js/scriptlets/dom-inspector.js | 215 +++++++++-------- src/logger-ui.html | 1 + 10 files changed, 517 insertions(+), 421 deletions(-) create mode 100644 platform/chromium/vapi-client-extra.js diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 3f4d4cc79..920eed3f3 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -939,10 +939,10 @@ vAPI.messaging = { switch ( msg.what ) { case 'connectionAccepted': case 'connectionRefused': { - const { port: toPort } = this.ports.get(msg.fromToken); + const toPort = this.ports.get(msg.fromToken); if ( toPort !== undefined ) { msg.tabId = tabId; - toPort.postMessage(request); + toPort.port.postMessage(request); } else { msg.what = 'connectionBroken'; port.postMessage(request); @@ -952,24 +952,32 @@ vAPI.messaging = { case 'connectionRequested': msg.tabId = tabId; for ( const { port: toPort } of this.ports.values() ) { + if ( toPort === port ) { continue; } toPort.postMessage(request); } break; case 'connectionBroken': case 'connectionCheck': case 'connectionMessage': { - const { port: toPort } = this.ports.get( + const toPort = this.ports.get( port.name === msg.fromToken ? msg.toToken : msg.fromToken ); if ( toPort !== undefined ) { msg.tabId = tabId; - toPort.postMessage(request); + toPort.port.postMessage(request); } else { msg.what = 'connectionBroken'; port.postMessage(request); } break; } + case 'extendClient': + vAPI.tabs.executeScript(tabId, { + file: '/js/vapi-client-extra.js', + }).then(( ) => { + callback(); + }); + break; case 'userCSS': if ( tabId === undefined ) { break; } const details = { @@ -1043,7 +1051,7 @@ vAPI.messaging = { } // Content process to main process: framework handler. - if ( request.channelName === 'vapi' ) { + if ( request.channel === 'vapi' ) { this.onFrameworkMessage(request, port, callback); return; } @@ -1052,7 +1060,7 @@ vAPI.messaging = { const fromDetails = this.ports.get(port.name); if ( fromDetails === undefined ) { return; } - const listenerDetails = this.listeners.get(request.channelName); + const listenerDetails = this.listeners.get(request.channel); let r = this.UNHANDLED; if ( (listenerDetails !== undefined) && diff --git a/platform/chromium/vapi-client-extra.js b/platform/chromium/vapi-client-extra.js new file mode 100644 index 000000000..f18280e04 --- /dev/null +++ b/platform/chromium/vapi-client-extra.js @@ -0,0 +1,308 @@ +/******************************************************************************* + + 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' || + vAPI.messaging instanceof Object === false || + 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); + } + static async 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 +})(); + +/******************************************************************************/ + +// Broadcast listening ability + +(( ) => { +// >>>>>>>> start of private namespace + +if ( + typeof vAPI !== 'object' || + vAPI.messaging instanceof Object === false || + vAPI.broadcastListener instanceof Object +) { + return; +} + +const listeners = new Set(); + +vAPI.broadcastListener = { + add: function(listener) { + listeners.add(listener); + vAPI.messaging.getPort(); + }, + remove: function(listener) { + listeners.delete(listener); + }, + canDestroyPort() { + return listeners.size === 0; + }, + mustDestroyPort() { + listeners.clear(); + }, + canProcessMessage(details) { + if ( details.broadcast === false ) { return; } + for ( const listener of listeners ) { + listener(details.msg); + } + }, +}; + +vAPI.messaging.extensions.push(vAPI.broadcastListener); + +// <<<<<<<< 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/chromium/vapi-client.js b/platform/chromium/vapi-client.js index 5acb284c4..3a5bc5d5c 100644 --- a/platform/chromium/vapi-client.js +++ b/platform/chromium/vapi-client.js @@ -30,16 +30,18 @@ // Skip if already injected. // >>>>>>>> start of HUGE-IF-BLOCK -if ( typeof vAPI === 'object' && !vAPI.clientScript ) { +if ( + typeof vAPI === 'object' && + vAPI.randomToken instanceof Function === false +) { /******************************************************************************/ /******************************************************************************/ -vAPI.clientScript = true; - vAPI.randomToken = function() { - return String.fromCharCode(Date.now() % 26 + 97) + - Math.floor(Math.random() * 982451653 + 982451653).toString(36); + const now = Date.now(); + return String.fromCharCode(now % 26 + 97) + + Math.floor((1 + Math.random()) * now).toString(36); }; vAPI.sessionId = vAPI.randomToken(); @@ -77,121 +79,11 @@ vAPI.messaging = { port: null, portTimer: null, portTimerDelay: 10000, - channels: new Map(), - connections: new Map(), - pending: new Map(), + extensions: [], msgIdGenerator: 1, + pending: new Map(), shuttingDown: false, - Connection: 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 = this.messaging.Connection; - if ( ctor.pagehide !== undefined ) { return; } - ctor.pagehide = ( ) => { - for ( const connection of this.messaging.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; - } - this.messaging.connections.delete(this.id); - const port = this.messaging.getPort(); - if ( port === null ) { return; } - port.postMessage({ - channelName: '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 ( this.messaging.connections.has(this.id) === false ) { return; } - const port = this.messaging.getPort(); - if ( port === null ) { return; } - port.postMessage({ - channelName: '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': - this.messaging.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 ( this.messaging.connections.has(this.id) ) { - this.checkAsync(); - } else { - details.what = 'connectionBroken'; - port.postMessage({ channelName: 'vapi', msg: details }); - } - break; - case 'connectionRefused': - this.messaging.connections.delete(this.id); - this.handler(details); - break; - } - } - send(payload) { - const port = this.messaging.getPort(); - if ( port === null ) { return; } - port.postMessage({ - channelName: 'vapi', - msg: this.toDetails('connectionMessage', payload) - }); - } - }, - shutdown: function() { this.shuttingDown = true; this.destroyPort(); @@ -212,14 +104,6 @@ vAPI.messaging = { messageListener: function(details) { if ( details instanceof Object === false ) { return; } - // Sent to all channels - if ( details.broadcast ) { - for ( const channelName of this.channels.keys() ) { - this.sendToChannelListeners(channelName, details.msg); - } - return; - } - // Response to specific message previously sent if ( details.msgId !== undefined ) { const resolver = this.pending.get(details.msgId); @@ -230,53 +114,28 @@ vAPI.messaging = { } } - if ( details.channelName !== 'vapi' ) { return; } - - // Internal handler - let connection; - - switch ( details.msg.what ) { - case 'connectionAccepted': - case 'connectionBroken': - case 'connectionCheck': - case 'connectionMessage': - case 'connectionRefused': - connection = this.connections.get(details.msg.id); - if ( connection === undefined ) { return; } - connection.receive(details.msg); - break; - case 'connectionRequested': - const listeners = this.channels.get(details.msg.to); - if ( listeners === undefined ) { return; } - const port = this.getPort(); - if ( port === null ) { return; } - for ( const listener of listeners ) { - if ( listener(details.msg) !== true ) { continue; } - details.msg.what = 'connectionAccepted'; - details.msg.toToken = port.name; - connection = new this.Connection(listener, details.msg); - this.connections.set(connection.id, connection); - break; - } - if ( details.msg.what !== 'connectionAccepted' ) { - details.msg.what = 'connectionRefused'; - } - port.postMessage(details); - break; - default: - break; - } + // Unhandled messages + this.extensions.every(ext => ext.canProcessMessage(details) !== true); }, messageListenerCallback: 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; + }, + portPoller: function() { this.portTimer = null; - if ( - this.port !== null && - this.channels.size === 0 && - this.connections.size === 0 && - this.pending.size === 0 - ) { + if ( this.port !== null && this.canDestroyPort() ) { return this.destroyPort(); } this.portTimer = vAPI.setTimeout(this.portPollerBound, this.portTimerDelay); @@ -296,13 +155,7 @@ vAPI.messaging = { port.onDisconnect.removeListener(this.disconnectListenerBound); this.port = null; } - this.channels.clear(); - if ( this.connections.size !== 0 ) { - for ( const connection of this.connections.values() ) { - connection.receive({ what: 'connectionBroken' }); - } - this.connections.clear(); - } + this.mustDestroyPort(); // service pending callbacks if ( this.pending.size !== 0 ) { const pending = this.pending; @@ -347,7 +200,7 @@ vAPI.messaging = { return this.port !== null ? this.port : this.createPort(); }, - send: function(channelName, msg) { + send: function(channel, msg) { // Too large a gap between the last request and the last response means // the main process is no longer reachable: memory leaks and bad // performance become a risk -- especially for long-lived, dynamic @@ -363,86 +216,12 @@ vAPI.messaging = { const promise = new Promise(resolve => { this.pending.set(msgId, resolve); }); - port.postMessage({ channelName, msgId, msg }); + port.postMessage({ channel, msgId, msg }); return promise; }, - - connectTo: function(from, to, handler) { - const port = this.getPort(); - if ( port === null ) { return; } - const connection = new this.Connection(handler, { - id: `${from}-${to}-${vAPI.sessionId}`, - to: to, - from: from, - fromToken: port.name - }); - this.connections.set(connection.id, connection); - port.postMessage({ - channelName: 'vapi', - msg: { - what: 'connectionRequested', - id: connection.id, - from: from, - fromToken: port.name, - to: to - } - }); - return connection.id; - }, - - disconnectFrom: function(connectionId) { - const connection = this.connections.get(connectionId); - if ( connection === undefined ) { return; } - connection.disconnect(); - }, - - sendTo: function(connectionId, payload) { - const connection = this.connections.get(connectionId); - if ( connection === undefined ) { return; } - connection.send(payload); - }, - - addChannelListener: function(channelName, listener) { - const listeners = this.channels.get(channelName); - if ( listeners === undefined ) { - this.channels.set(channelName, [ listener ]); - } else if ( listeners.indexOf(listener) === -1 ) { - listeners.push(listener); - } - this.getPort(); - }, - - removeChannelListener: function(channelName, listener) { - const listeners = this.channels.get(channelName); - if ( listeners === undefined ) { return; } - const pos = listeners.indexOf(listener); - if ( pos === -1 ) { return; } - listeners.splice(pos, 1); - if ( listeners.length === 0 ) { - this.channels.delete(channelName); - } - }, - - removeAllChannelListeners: function(channelName) { - this.channels.delete(channelName); - }, - - sendToChannelListeners: function(channelName, msg) { - let listeners = this.channels.get(channelName); - if ( listeners === undefined ) { return; } - listeners = listeners.slice(0); - let response; - for ( const listener of listeners ) { - response = listener(msg); - if ( response !== undefined ) { break; } - } - return response; - } }; -/******************************************************************************/ - -vAPI.shutdown.add(function() { +vAPI.shutdown.add(( ) => { vAPI.messaging.shutdown(); window.vAPI = undefined; }); diff --git a/src/3p-filters.html b/src/3p-filters.html index b35db0f1d..cda8e41b0 100644 --- a/src/3p-filters.html +++ b/src/3p-filters.html @@ -59,6 +59,7 @@ + diff --git a/src/js/3p-filters.js b/src/js/3p-filters.js index 10d262b53..31c80273c 100644 --- a/src/js/3p-filters.js +++ b/src/js/3p-filters.js @@ -38,7 +38,9 @@ let hideUnusedSet = new Set(); /******************************************************************************/ -const onMessage = function(msg) { +const messaging = vAPI.messaging; + +vAPI.broadcastListener.add(msg => { switch ( msg.what ) { case 'assetUpdated': updateAssetStatus(msg); @@ -53,10 +55,7 @@ const onMessage = function(msg) { default: break; } -}; - -const messaging = vAPI.messaging; -messaging.addChannelListener('dashboard', onMessage); +}); /******************************************************************************/ diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 2a42e49c8..20f8f1f3d 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -817,8 +817,9 @@ FilterContainer.prototype.pruneSelectorCacheAsync = function() { /******************************************************************************/ FilterContainer.prototype.randomAlphaToken = function() { - return String.fromCharCode(Date.now() % 26 + 97) + - Math.floor(Math.random() * 982451653 + 982451653).toString(36); + const now = Date.now(); + return String.fromCharCode(now % 26 + 97) + + Math.floor((1 + Math.random()) * now).toString(36); }; /******************************************************************************/ diff --git a/src/js/logger-ui-inspector.js b/src/js/logger-ui-inspector.js index d321496af..17fdb5cad 100644 --- a/src/js/logger-ui-inspector.js +++ b/src/js/logger-ui-inspector.js @@ -55,9 +55,10 @@ var filterToIdMap = new Map(); /******************************************************************************/ -var messaging = vAPI.messaging; +const messaging = vAPI.messaging; -messaging.addChannelListener('loggerUI', function(msg) { +vAPI.MessagingConnection.addListener(function(msg) { + if ( msg.from !== 'domInspector' || msg.to !== 'loggerUI' ) { return; } switch ( msg.what ) { case 'connectionBroken': if ( inspectorConnectionId === msg.id ) { @@ -77,12 +78,8 @@ messaging.addChannelListener('loggerUI', function(msg) { } break; case 'connectionRequested': - if ( msg.from !== 'domInspector' ) { return false; } - if ( - msg.tabId === undefined || - msg.tabId !== inspectedTabId - ) { - return false; + if ( msg.tabId === undefined || msg.tabId !== inspectedTabId ) { + return; } filterToIdMap.clear(); logger.removeAllChildren(domTree); @@ -93,7 +90,7 @@ messaging.addChannelListener('loggerUI', function(msg) { /******************************************************************************/ -var nodeFromDomEntry = function(entry) { +const nodeFromDomEntry = function(entry) { var node, value; var li = document.createElement('li'); li.setAttribute('id', entry.nid); @@ -130,25 +127,21 @@ var nodeFromDomEntry = function(entry) { /******************************************************************************/ -var appendListItem = function(ul, li) { +const appendListItem = function(ul, li) { ul.appendChild(li); // Ancestor nodes of a node which is affected by a cosmetic filter will // be marked as "containing cosmetic filters", for user convenience. - if ( li.classList.contains('isCosmeticHide') === false ) { - return; - } + if ( li.classList.contains('isCosmeticHide') === false ) { return; } for (;;) { li = li.parentElement.parentElement; - if ( li === null ) { - break; - } + if ( li === null ) { break; } li.classList.add('hasCosmeticHide'); } }; /******************************************************************************/ -var renderDOMFull = function(response) { +const renderDOMFull = function(response) { var domTreeParent = domTree.parentElement; var ul = domTreeParent.removeChild(domTree); logger.removeAllChildren(domTree); @@ -197,7 +190,7 @@ var renderDOMFull = function(response) { /******************************************************************************/ -var patchIncremental = function(from, delta) { +const patchIncremental = function(from, delta) { var span, cnt; var li = from.parentElement.parentElement; var patchCosmeticHide = delta >= 0 && @@ -222,7 +215,7 @@ var patchIncremental = function(from, delta) { /******************************************************************************/ -var renderDOMIncremental = function(response) { +const renderDOMIncremental = function(response) { // Process each journal entry: // 1 = node added // -1 = node removed @@ -284,7 +277,7 @@ var renderDOMIncremental = function(response) { /******************************************************************************/ -var countFromNode = function(li) { +const countFromNode = function(li) { var span = li.children[2]; var cnt = parseInt(span.getAttribute('data-cnt'), 10); return isNaN(cnt) ? 0 : cnt; @@ -292,7 +285,7 @@ var countFromNode = function(li) { /******************************************************************************/ -var selectorFromNode = function(node) { +const selectorFromNode = function(node) { var selector = ''; var code; while ( node !== null ) { @@ -312,7 +305,7 @@ var selectorFromNode = function(node) { /******************************************************************************/ -var selectorFromFilter = function(node) { +const selectorFromFilter = function(node) { while ( node !== null ) { if ( node.localName === 'li' ) { var code = node.querySelector('code:nth-of-type(2)'); @@ -327,7 +320,7 @@ var selectorFromFilter = function(node) { /******************************************************************************/ -var nidFromNode = function(node) { +const nidFromNode = function(node) { var li = node; while ( li !== null ) { if ( li.localName === 'li' ) { @@ -399,7 +392,7 @@ const startDialog = (function() { }; const showCommitted = function() { - messaging.sendTo(inspectorConnectionId, { + vAPI.MessagingConnection.sendTo(inspectorConnectionId, { what: 'showCommitted', hide: hideSelectors.join(',\n'), unhide: unhideSelectors.join(',\n') @@ -407,7 +400,7 @@ const startDialog = (function() { }; const showInteractive = function() { - messaging.sendTo(inspectorConnectionId, { + vAPI.MessagingConnection.sendTo(inspectorConnectionId, { what: 'showInteractive', hide: hideSelectors.join(',\n'), unhide: unhideSelectors.join(',\n') @@ -460,7 +453,7 @@ const startDialog = (function() { /******************************************************************************/ -var onClicked = function(ev) { +const onClicked = function(ev) { ev.stopPropagation(); if ( inspectedTabId === 0 ) { return; } @@ -489,7 +482,7 @@ var onClicked = function(ev) { // Toggle cosmetic filter if ( target.classList.contains('filter') ) { - messaging.sendTo(inspectorConnectionId, { + vAPI.MessagingConnection.sendTo(inspectorConnectionId, { what: 'toggleFilter', original: false, target: target.classList.toggle('off'), @@ -504,7 +497,7 @@ var onClicked = function(ev) { } // Toggle node else { - messaging.sendTo(inspectorConnectionId, { + vAPI.MessagingConnection.sendTo(inspectorConnectionId, { what: 'toggleNodes', original: true, target: target.classList.toggle('off') === false, @@ -520,13 +513,13 @@ var onClicked = function(ev) { /******************************************************************************/ -var onMouseOver = (function() { +const onMouseOver = (function() { var mouseoverTarget = null; var mouseoverTimer = null; var timerHandler = function() { mouseoverTimer = null; - messaging.sendTo(inspectorConnectionId, { + vAPI.MessagingConnection.sendTo(inspectorConnectionId, { what: 'highlightOne', selector: selectorFromNode(mouseoverTarget), nid: nidFromNode(mouseoverTarget), @@ -574,9 +567,9 @@ const injectInspector = function() { /******************************************************************************/ -var shutdownInspector = function() { +const shutdownInspector = function() { if ( inspectorConnectionId !== undefined ) { - messaging.disconnectFrom(inspectorConnectionId); + vAPI.MessagingConnection.disconnectFrom(inspectorConnectionId); inspectorConnectionId = undefined; } logger.removeAllChildren(domTree); @@ -586,7 +579,7 @@ var shutdownInspector = function() { /******************************************************************************/ -var onTabIdChanged = function() { +const onTabIdChanged = function() { const tabId = currentTabId(); if ( tabId <= 0 ) { return toggleOff(); @@ -599,7 +592,7 @@ var onTabIdChanged = function() { /******************************************************************************/ -var toggleVCompactView = function() { +const toggleVCompactView = function() { var state = inspector.classList.toggle('vExpanded'); var branches = document.querySelectorAll('#domInspector li.branch'); for ( var branch of branches ) { @@ -607,14 +600,14 @@ var toggleVCompactView = function() { } }; -var toggleHCompactView = function() { +const toggleHCompactView = function() { inspector.classList.toggle('hCompact'); }; /******************************************************************************/ /* var toggleHighlightMode = function() { - messaging.sendTo(inspectorConnectionId, { + vAPI.MessagingConnection.sendTo(inspectorConnectionId, { what: 'highlightMode', invert: uDom.nodeFromSelector('#domInspector .permatoolbar .highlightMode').classList.toggle('invert') }); @@ -622,9 +615,12 @@ var toggleHighlightMode = function() { */ /******************************************************************************/ -var revert = function() { +const revert = function() { uDom('#domTree .off').removeClass('off'); - messaging.sendTo(inspectorConnectionId, { what: 'resetToggledNodes' }); + vAPI.MessagingConnection.sendTo( + inspectorConnectionId, + { what: 'resetToggledNodes' } + ); inspector.querySelector('.permatoolbar .revert').classList.add('disabled'); inspector.querySelector('.permatoolbar .commit').classList.add('disabled'); }; diff --git a/src/js/scriptlets/cosmetic-logger.js b/src/js/scriptlets/cosmetic-logger.js index ab3a3f721..9af938969 100644 --- a/src/js/scriptlets/cosmetic-logger.js +++ b/src/js/scriptlets/cosmetic-logger.js @@ -24,6 +24,7 @@ /******************************************************************************/ (( ) => { +// >>>>>>>> start of private namespace /******************************************************************************/ @@ -295,21 +296,28 @@ const handlers = { /******************************************************************************/ -const onMessage = function(msg) { - if ( msg.what === 'loggerDisabled' ) { - processTimer.clear(); - attributeObserver.disconnect(); - vAPI.domFilterer.removeListener(handlers); - vAPI.domWatcher.removeListener(handlers); - vAPI.messaging.removeChannelListener('domLogger', onMessage); +(async ( ) => { + // Dynamically add broadcast listening abilities. + if ( vAPI.broadcastListener instanceof Object === false ) { + await vAPI.messaging.send('vapi', { what: 'extendClient' }); } -}; -vAPI.messaging.addChannelListener('domLogger', onMessage); + const broadcastListener = msg => { + if ( msg.what === 'loggerDisabled' ) { + processTimer.clear(); + attributeObserver.disconnect(); + vAPI.domFilterer.removeListener(handlers); + vAPI.domWatcher.removeListener(handlers); + vAPI.broadcastListener.remove(broadcastListener); + } + }; + vAPI.broadcastListener.add(broadcastListener); +})(); vAPI.domWatcher.addListener(handlers); /******************************************************************************/ +// <<<<<<<< end of private namespace })(); diff --git a/src/js/scriptlets/dom-inspector.js b/src/js/scriptlets/dom-inspector.js index 230757e0c..22805e97e 100644 --- a/src/js/scriptlets/dom-inspector.js +++ b/src/js/scriptlets/dom-inspector.js @@ -19,18 +19,16 @@ Home: https://github.com/gorhill/uBlock */ -/******************************************************************************/ -/******************************************************************************/ - -(function() { - 'use strict'; +/******************************************************************************/ /******************************************************************************/ -if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { - return; -} +(( ) => { + +/******************************************************************************/ + +if ( typeof vAPI !== 'object' || !vAPI.domFilterer ) { return; } /******************************************************************************/ @@ -49,7 +47,7 @@ if ( document.querySelector('iframe.dom-inspector.' + sessionId) !== null ) { // Added serializeAsString parameter. /*! http://mths.be/cssescape v0.2.1 by @mathias | MIT license */ -var cssEscape = (function(/*root*/) { +const cssEscape = (function(/*root*/) { var InvalidCharacterError = function(message) { this.message = message; @@ -137,29 +135,29 @@ var cssEscape = (function(/*root*/) { /******************************************************************************/ /******************************************************************************/ -var loggerConnectionId; +let loggerConnectionId; // Highlighter-related -var svgRoot = null; -var pickerRoot = null; +let svgRoot = null; +let pickerRoot = null; -var nodeToIdMap = new WeakMap(); // No need to iterate +let nodeToIdMap = new WeakMap(); // No need to iterate -var blueNodes = []; -var roRedNodes = new Map(); // node => current cosmetic filter -var rwRedNodes = new Set(); // node => new cosmetic filter (toggle node) +let blueNodes = []; +const roRedNodes = new Map(); // node => current cosmetic filter +const rwRedNodes = new Set(); // node => new cosmetic filter (toggle node) //var roGreenNodes = new Map(); // node => current exception cosmetic filter (can't toggle) -var rwGreenNodes = new Set(); // node => new exception cosmetic filter (toggle filter) +const rwGreenNodes = new Set(); // node => new exception cosmetic filter (toggle filter) -var reHasCSSCombinators = /[ >+~]/; +const reHasCSSCombinators = /[ >+~]/; /******************************************************************************/ -var domLayout = (function() { - var skipTagNames = new Set([ +const domLayout = (function() { + const skipTagNames = new Set([ 'br', 'head', 'link', 'meta', 'script', 'style', 'title' ]); - var resourceAttrNames = new Map([ + const resourceAttrNames = new Map([ [ 'a', 'href' ], [ 'iframe', 'src' ], [ 'img', 'src' ], @@ -170,13 +168,13 @@ var domLayout = (function() { // This will be used to uniquely identify nodes across process. - var newNodeId = function(node) { + const newNodeId = function(node) { var nid = 'n' + (idGenerator++).toString(36); nodeToIdMap.set(node, nid); return nid; }; - var selectorFromNode = function(node) { + const selectorFromNode = function(node) { var str, attr, pos, sw, i; var tag = node.localName; var selector = cssEscape(tag); @@ -217,7 +215,7 @@ var domLayout = (function() { return selector; }; - var DomRoot = function() { + const DomRoot = function() { this.nid = newNodeId(document.body); this.lvl = 0; this.sel = 'body'; @@ -225,7 +223,7 @@ var domLayout = (function() { this.filter = roRedNodes.get(document.body); }; - var DomNode = function(node, level) { + const DomNode = function(node, level) { this.nid = newNodeId(node); this.lvl = level; this.sel = selectorFromNode(node); @@ -233,7 +231,7 @@ var domLayout = (function() { this.filter = roRedNodes.get(node); }; - var domNodeFactory = function(level, node) { + const domNodeFactory = function(level, node) { var localName = node.localName; if ( skipTagNames.has(localName) ) { return null; } // skip uBlock's own nodes @@ -246,7 +244,7 @@ var domLayout = (function() { // Collect layout data. - var getLayoutData = function() { + const getLayoutData = function() { var layout = []; var stack = []; var node = document.documentElement; @@ -282,7 +280,7 @@ var domLayout = (function() { // Descendant count for each node. - var patchLayoutData = function(layout) { + const patchLayoutData = function(layout) { var stack = [], ptr; var lvl = 0; var domNode, cnt; @@ -320,7 +318,7 @@ var domLayout = (function() { var addedNodelists = []; var removedNodelist = []; - var previousElementSiblingId = function(node) { + const previousElementSiblingId = function(node) { var sibling = node; for (;;) { sibling = sibling.previousElementSibling; @@ -330,7 +328,7 @@ var domLayout = (function() { } }; - var journalFromBranch = function(root, newNodes, newNodeToIdMap) { + const journalFromBranch = function(root, newNodes, newNodeToIdMap) { var domNode; var node = root.firstElementChild; while ( node !== null ) { @@ -361,7 +359,7 @@ var domLayout = (function() { } }; - var journalFromMutations = function() { + const journalFromMutations = function() { var nodelist, node, domNode, nid; mutationTimer = undefined; @@ -408,7 +406,7 @@ var domLayout = (function() { if ( journalEntries.length === 0 ) { return; } - vAPI.messaging.sendTo(loggerConnectionId, { + vAPI.MessagingConnection.sendTo(loggerConnectionId, { what: 'domLayoutIncremental', url: window.location.href, hostname: window.location.hostname, @@ -417,7 +415,7 @@ var domLayout = (function() { }); }; - var onMutationObserved = function(mutationRecords) { + const onMutationObserved = function(mutationRecords) { for ( var record of mutationRecords ) { if ( record.addedNodes.length !== 0 ) { addedNodelists.push(record.addedNodes); @@ -433,7 +431,7 @@ var domLayout = (function() { // API - var getLayout = function() { + const getLayout = function() { cosmeticFilterMapper.reset(); mutationObserver = new MutationObserver(onMutationObserved); mutationObserver.observe(document.body, { @@ -449,11 +447,11 @@ var domLayout = (function() { }; }; - var reset = function() { + const reset = function() { shutdown(); }; - var shutdown = function() { + const shutdown = function() { if ( mutationTimer !== undefined ) { clearTimeout(mutationTimer); mutationTimer = undefined; @@ -482,7 +480,7 @@ var domLayout = (function() { // For browsers not supporting `:scope`, it's not the end of the world: the // suggested CSS selectors may just end up being more verbose. -var cssScope = ':scope > '; +let cssScope = ':scope > '; try { document.querySelector(':scope *'); } catch (e) { @@ -491,7 +489,7 @@ try { /******************************************************************************/ -var cosmeticFilterMapper = (function() { +const cosmeticFilterMapper = (function() { // https://github.com/gorhill/uBlock/issues/546 var matchesFnName; if ( typeof document.body.matches === 'function' ) { @@ -502,7 +500,7 @@ var cosmeticFilterMapper = (function() { matchesFnName = 'webkitMatchesSelector'; } - var nodesFromStyleTag = function(rootNode) { + const nodesFromStyleTag = function(rootNode) { var filterMap = roRedNodes, entry, selector, canonical, nodes, node; @@ -544,16 +542,16 @@ var cosmeticFilterMapper = (function() { } }; - var incremental = function(rootNode) { + const incremental = function(rootNode) { nodesFromStyleTag(rootNode); }; - var reset = function() { - roRedNodes = new Map(); + const reset = function() { + roRedNodes.clear(); incremental(document.documentElement); }; - var shutdown = function() { + const shutdown = function() { vAPI.domFilterer.toggle(true); }; @@ -566,26 +564,23 @@ var cosmeticFilterMapper = (function() { /******************************************************************************/ -var elementsFromSelector = function(selector, context) { +const elementsFromSelector = function(selector, context) { if ( !context ) { context = document; } - var out; if ( selector.indexOf(':') !== -1 ) { - out = elementsFromSpecialSelector(selector); - if ( out !== undefined ) { - return out; - } + const out = elementsFromSpecialSelector(selector); + if ( out !== undefined ) { return out; } } // plain CSS selector try { - out = context.querySelectorAll(selector); + return context.querySelectorAll(selector); } catch (ex) { } - return out || []; + return []; }; -var elementsFromSpecialSelector = function(selector) { +const elementsFromSpecialSelector = function(selector) { var out = [], i; var matches = /^(.+?):has\((.+?)\)$/.exec(selector); if ( matches !== null ) { @@ -606,36 +601,35 @@ var elementsFromSpecialSelector = function(selector) { } matches = /^:xpath\((.+?)\)$/.exec(selector); - if ( matches !== null ) { - var xpr = document.evaluate( - matches[1], - document, - null, - XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, - null - ); - i = xpr.snapshotLength; - while ( i-- ) { - out.push(xpr.snapshotItem(i)); - } - return out; + if ( matches === null ) { return; } + const xpr = document.evaluate( + matches[1], + document, + null, + XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, + null + ); + i = xpr.snapshotLength; + while ( i-- ) { + out.push(xpr.snapshotItem(i)); } + return out; }; /******************************************************************************/ -var getSvgRootChildren = function() { +const getSvgRootChildren = function() { if ( svgRoot.children ) { return svgRoot.children; } else { - var childNodes = Array.prototype.slice.apply(svgRoot.childNodes); + const childNodes = Array.prototype.slice.apply(svgRoot.childNodes); return childNodes.filter(function(node) { return node.nodeType === Node.ELEMENT_NODE; }); } }; -var highlightElements = function() { +const highlightElements = function() { var islands; var elem, rect, poly; var xl, xr, yt, yb, w, h, ws; @@ -729,9 +723,9 @@ var highlightElements = function() { /******************************************************************************/ -var onScrolled = (function() { - var buffered = false; - var timerHandler = function() { +const onScrolled = (function() { + let buffered = false; + const timerHandler = function() { buffered = false; highlightElements(); }; @@ -745,10 +739,10 @@ var onScrolled = (function() { /******************************************************************************/ -var selectNodes = function(selector, nid) { - var nodes = elementsFromSelector(selector); +const selectNodes = function(selector, nid) { + const nodes = elementsFromSelector(selector); if ( nid === '' ) { return nodes; } - for ( var node of nodes ) { + for ( const node of nodes ) { if ( nodeToIdMap.get(node) === nid ) { return [ node ]; } @@ -758,9 +752,9 @@ var selectNodes = function(selector, nid) { /******************************************************************************/ -var nodesFromFilter = function(selector) { - var out = []; - for ( var entry of roRedNodes ) { +const nodesFromFilter = function(selector) { + const out = []; + for ( const entry of roRedNodes ) { if ( entry[1] === selector ) { out.push(entry[0]); } @@ -770,8 +764,8 @@ var nodesFromFilter = function(selector) { /******************************************************************************/ -var toggleExceptions = function(nodes, targetState) { - for ( var node of nodes ) { +const toggleExceptions = function(nodes, targetState) { + for ( const node of nodes ) { if ( targetState ) { rwGreenNodes.add(node); } else { @@ -780,8 +774,8 @@ var toggleExceptions = function(nodes, targetState) { } }; -var toggleFilter = function(nodes, targetState) { - for ( var node of nodes ) { +const toggleFilter = function(nodes, targetState) { + for ( const node of nodes ) { if ( targetState ) { rwRedNodes.delete(node); } else { @@ -790,21 +784,19 @@ var toggleFilter = function(nodes, targetState) { } }; -var resetToggledNodes = function() { +const resetToggledNodes = function() { rwGreenNodes.clear(); rwRedNodes.clear(); }; -// https://www.youtube.com/watch?v=L5jRewnxSBY - /******************************************************************************/ -var start = function() { - var onReady = function(ev) { +const start = function() { + const onReady = function(ev) { if ( ev ) { document.removeEventListener(ev.type, onReady); } - vAPI.messaging.sendTo(loggerConnectionId, domLayout.get()); + vAPI.MessagingConnection.sendTo(loggerConnectionId, domLayout.get()); vAPI.domFilterer.toggle(false, highlightElements); }; if ( document.readyState === 'loading' ) { @@ -816,10 +808,10 @@ var start = function() { /******************************************************************************/ -var shutdown = function() { +const shutdown = function() { cosmeticFilterMapper.shutdown(); domLayout.shutdown(); - vAPI.messaging.disconnectFrom(loggerConnectionId); + vAPI.MessagingConnection.disconnectFrom(loggerConnectionId); window.removeEventListener('scroll', onScrolled, true); document.documentElement.removeChild(pickerRoot); pickerRoot = svgRoot = null; @@ -828,7 +820,7 @@ var shutdown = function() { /******************************************************************************/ /******************************************************************************/ -var onMessage = function(request) { +const onMessage = function(request) { var response, nodes; @@ -888,32 +880,17 @@ var onMessage = function(request) { return response; }; -var messagingHandler = function(msg) { - switch ( msg.what ) { - case 'connectionAccepted': - loggerConnectionId = msg.id; - start(); - break; - case 'connectionBroken': - shutdown(); - break; - case 'connectionMessage': - onMessage(msg.payload); - break; - } -}; - /******************************************************************************/ // Install DOM inspector widget -var bootstrap = function(ev) { +const bootstrap = async function(ev) { if ( ev ) { pickerRoot.removeEventListener(ev.type, bootstrap); } - var pickerDoc = this.contentDocument; + const pickerDoc = ev.target.contentDocument; - var style = pickerDoc.createElement('style'); + const style = pickerDoc.createElement('style'); style.textContent = [ 'body {', 'background-color: transparent;', @@ -955,7 +932,25 @@ var bootstrap = function(ev) { window.addEventListener('scroll', onScrolled, true); - vAPI.messaging.connectTo('domInspector', 'loggerUI', messagingHandler); + // Dynamically add direct connection abilities so that we can establish + // a direct, fast messaging connection to the logger. + if ( vAPI.MessagingConnection instanceof Function === false ) { + await vAPI.messaging.send('vapi', { what: 'extendClient' }); + } + 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'); @@ -982,7 +977,7 @@ pickerRoot.style.cssText = [ '' ].join(' !important;\n'); -pickerRoot.addEventListener('load', bootstrap); +pickerRoot.addEventListener('load', ev => { bootstrap(ev); }); document.documentElement.appendChild(pickerRoot); /******************************************************************************/ diff --git a/src/logger-ui.html b/src/logger-ui.html index f6b153e13..2202579f6 100644 --- a/src/logger-ui.html +++ b/src/logger-ui.html @@ -205,6 +205,7 @@ +