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