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.
This commit is contained in:
Raymond Hill 2019-09-19 08:31:38 -04:00
parent 149b5cf59c
commit 87d0e456f1
No known key found for this signature in database
GPG key ID: 25E1490B761470C2
10 changed files with 517 additions and 421 deletions

View file

@ -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) &&

View file

@ -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;

View file

@ -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;
});

View file

@ -59,6 +59,7 @@
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/dashboard-common.js"></script>

View file

@ -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);
});
/******************************************************************************/

View file

@ -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);
};
/******************************************************************************/

View file

@ -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');
};

View file

@ -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
})();

View file

@ -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);
/******************************************************************************/

View file

@ -205,6 +205,7 @@
<script src="js/vapi.js"></script>
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/logger-ui.js"></script>