diff --git a/platform/firefox/frameModule.js b/platform/firefox/frameModule.js index a11720e42..98fc107fa 100644 --- a/platform/firefox/frameModule.js +++ b/platform/firefox/frameModule.js @@ -30,6 +30,7 @@ const {Services} = Cu.import('resource://gre/modules/Services.jsm', null); const {XPCOMUtils} = Cu.import('resource://gre/modules/XPCOMUtils.jsm', null); const hostName = Services.io.newURI(Components.stack.filename, null, null).host; +const rpcEmitterName = hostName + ':child-process-message'; //Cu.import('resource://gre/modules/devtools/Console.jsm'); @@ -239,9 +240,25 @@ const contentObserver = { wantXHRConstructor: false }); + if ( Services.cpmm ) { + sandbox.rpc = function(details) { + var svc = Services; + if ( svc === undefined ) { return; } + var cpmm = svc.cpmm; + if ( !cpmm ) { return; } + var r = cpmm.sendSyncMessage(rpcEmitterName, details); + if ( Array.isArray(r) ) { + return r[0]; + } + }; + } else { + sandbox.rpc = function() {}; + } + sandbox.injectScript = function(script) { - if ( Services !== undefined ) { - Services.scriptloader.loadSubScript(script, sandbox); + var svc = Services; + if ( svc !== undefined ) { + svc.scriptloader.loadSubScript(script, sandbox); } else { // Sandbox appears void. // I've seen this happens, need to investigate why. @@ -258,9 +275,10 @@ const contentObserver = { sandbox.removeMessageListener(); sandbox.addMessageListener = sandbox.injectScript = + sandbox.outerShutdown = sandbox.removeMessageListener = - sandbox.sendAsyncMessage = - sandbox.outerShutdown = function(){}; + sandbox.rpc = + sandbox.sendAsyncMessage = function(){}; sandbox.vAPI = {}; messager = null; }; @@ -412,13 +430,19 @@ const LocationChangeListener = function(docShell) { var requestor = docShell.QueryInterface(Ci.nsIInterfaceRequestor); var ds = requestor.getInterface(Ci.nsIWebProgress); - var mm = requestor.getInterface(Ci.nsIContentFrameMessageManager); - - if ( ds && mm && typeof mm.sendAsyncMessage === 'function' ) { - this.docShell = ds; - this.messageManager = mm; - ds.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); + if ( !ds ) { + return; } + var mm = requestor.getInterface(Ci.nsIContentFrameMessageManager); + if ( !mm ) { + return; + } + if ( typeof mm.sendAsyncMessage !== 'function' ) { + return; + } + this.docShell = ds; + this.messageManager = mm; + ds.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION); }; LocationChangeListener.prototype.QueryInterface = XPCOMUtils.generateQI([ diff --git a/platform/firefox/frameScript.js b/platform/firefox/frameScript.js index fdee50c59..17cd84f2f 100644 --- a/platform/firefox/frameScript.js +++ b/platform/firefox/frameScript.js @@ -19,13 +19,11 @@ Home: https://github.com/gorhill/uBlock */ -/* global addMessageListener, removeMessageListener, docShell */ - /******************************************************************************/ -var locationChangeListener; // Keep alive while frameScript is alive +// https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Frame_script_environment -(function() { +(function(context) { 'use strict'; @@ -52,25 +50,36 @@ let injectContentScripts = function(win) { }; let onLoadCompleted = function() { - removeMessageListener('ublock0-load-completed', onLoadCompleted); - injectContentScripts(content); + context.removeMessageListener('ublock0-load-completed', onLoadCompleted); + injectContentScripts(context.content); }; +context.addMessageListener('ublock0-load-completed', onLoadCompleted); -addMessageListener('ublock0-load-completed', onLoadCompleted); +let shutdown = function(ev) { + if ( ev.target !== context ) { + return; + } + context.removeMessageListener('ublock0-load-completed', onLoadCompleted); + context.removeEventListener('unload', shutdown); + context.locationChangeListener = null; + LocationChangeListener = null; + contentObserver = null; +}; +context.addEventListener('unload', shutdown); -if ( docShell ) { +if ( context.docShell ) { let Ci = Components.interfaces; - let wp = docShell.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebProgress); + let wp = context.docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); let dw = wp.DOMWindow; if ( dw === dw.top ) { - locationChangeListener = new LocationChangeListener(docShell); + context.locationChangeListener = new LocationChangeListener(context.docShell); } } /******************************************************************************/ -})(); +})(this); /******************************************************************************/ diff --git a/platform/firefox/vapi-background.js b/platform/firefox/vapi-background.js index bf531c050..8bc110b15 100644 --- a/platform/firefox/vapi-background.js +++ b/platform/firefox/vapi-background.js @@ -70,7 +70,7 @@ vAPI.localStorage.setDefaultBool('forceLegacyToolbarButton', false); var cleanupTasks = []; // This must be updated manually, every time a new task is added/removed -var expectedNumberOfCleanups = 7; +var expectedNumberOfCleanups = 8; window.addEventListener('unload', function() { if ( typeof vAPI.app.onShutdown === 'function' ) { @@ -90,10 +90,11 @@ window.addEventListener('unload', function() { } // frameModule needs to be cleared too + var frameModuleURL = vAPI.getURL('frameModule.js'); var frameModule = {}; - Cu.import(vAPI.getURL('frameModule.js'), frameModule); + Cu.import(frameModuleURL, frameModule); frameModule.contentObserver.unregister(); - Cu.unload(vAPI.getURL('frameModule.js')); + Cu.unload(frameModuleURL); }); /******************************************************************************/ @@ -987,15 +988,15 @@ var tabWatcher = (function() { }; // https://developer.mozilla.org/en-US/docs/Web/Events/TabOpen - var onOpen = function({target}) { - var tabId = tabIdFromTarget(target); - var browser = browserFromTabId(tabId); - vAPI.tabs.onNavigation({ - frameId: 0, - tabId: tabId, - url: browser.currentURI.asciiSpec, - }); - }; + //var onOpen = function({target}) { + // var tabId = tabIdFromTarget(target); + // var browser = browserFromTabId(tabId); + // vAPI.tabs.onNavigation({ + // frameId: 0, + // tabId: tabId, + // url: browser.currentURI.asciiSpec, + // }); + //}; // https://developer.mozilla.org/en-US/docs/Web/Events/TabShow var onShow = function({target}) { @@ -1208,7 +1209,7 @@ vAPI.messaging = { return Cc['@mozilla.org/globalmessagemanager;1'] .getService(Ci.nsIMessageListenerManager); }, - frameScript: vAPI.getURL('frameScript.js'), + frameScriptURL: vAPI.getURL('frameScript.js'), listeners: {}, defaultHandler: null, NOOPFUNC: function(){}, @@ -1438,7 +1439,7 @@ vAPI.messaging.setup = function(defaultHandler) { this.onMessage ); - this.globalMessageManager.loadFrameScript(this.frameScript, true); + this.globalMessageManager.loadFrameScript(this.frameScriptURL, true); cleanupTasks.push(function() { var gmm = vAPI.messaging.globalMessageManager; @@ -1452,7 +1453,7 @@ vAPI.messaging.setup = function(defaultHandler) { }) ); - gmm.removeDelayedFrameScript(vAPI.messaging.frameScript); + gmm.removeDelayedFrameScript(vAPI.messaging.frameScriptURL); gmm.removeMessageListener( location.host + ':background', vAPI.messaging.onMessage @@ -1471,6 +1472,60 @@ vAPI.messaging.broadcast = function(message) { ); }; +/******************************************************************************/ +/******************************************************************************/ + +// Synchronous messaging: Firefox allows this. Chromium does not allow this. + +// Sometimes there is no way around synchronous messaging, as long as: +// - the code at the other end execute fast and return quickly. +// - it's not abused. +// Original rationale is . +// Synchronous messaging is a good solution for this case because: +// - It's done only *once* per page load. (Keep in mind there is already a +// sync message sent for each single network request on a page and it's not +// an issue, because the code executed is trivial, which is the key -- see +// shouldLoadListener below). +// - The code at the other end is fast. +// Though vAPI.rpcReceiver was brought forth because of this one case, I +// generalized the concept for whatever future need for synchronous messaging +// which might arise. + +// https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox/Message_Manager/Message_manager_overview#Content_frame_message_manager + +vAPI.rpcReceiver = (function() { + var calls = Object.create(null); + var childProcessMessageName = location.host + ':child-process-message'; + + var onChildProcessMessage = function(ev) { + var msg = ev.data; + if ( !msg ) { return; } + var fn = calls[msg.fnName]; + if ( typeof fn === 'function' ) { + return fn(msg); + } + }; + + if ( Services.ppmm ) { + Services.ppmm.addMessageListener( + childProcessMessageName, + onChildProcessMessage + ); + } + + cleanupTasks.push(function() { + if ( Services.ppmm ) { + Services.ppmm.removeMessageListener( + childProcessMessageName, + onChildProcessMessage + ); + } + }); + + return calls; +})(); + +/******************************************************************************/ /******************************************************************************/ var httpObserver = { @@ -1865,7 +1920,11 @@ vAPI.net.registerListeners = function() { var tabId = tabWatcher.tabIdFromTarget(e.target); var sourceTabId = null; - // Popup candidate + // Popup candidate: this code path is taken only for when a new top + // document loads, i.e. only once per document load. TODO: evaluate for + // popup filtering in an asynchrous manner -- it's not really required + // to evaluate on the spot. Still, there is currently no harm given + // this code path is typically taken only once per page load. if ( details.openerURL ) { for ( var browser of tabWatcher.browsers() ) { var URI = browser.currentURI; @@ -1899,8 +1958,9 @@ vAPI.net.registerListeners = function() { } } - //console.log('shouldLoadListener:', details.url); - + // We are being called synchronously from the content process, so we + // must return ASAP. The code below merely record the details of the + // request into a ring buffer for later retrieval by the HTTP observer. var pendingReq = httpObserver.createPendingRequest(details.url); pendingReq.frameId = details.frameId; pendingReq.parentFrameId = details.parentFrameId; diff --git a/platform/firefox/vapi-client.js b/platform/firefox/vapi-client.js index bd6facd29..07da7c84a 100644 --- a/platform/firefox/vapi-client.js +++ b/platform/firefox/vapi-client.js @@ -31,6 +31,13 @@ /******************************************************************************/ +// Not all sandbox are given an rpc function, so assign a dummy one it is +// missing -- this avoids the need for constantly testing before use. + +self.rpc = self.rpc || function(){}; + +/******************************************************************************/ + var vAPI = self.vAPI = self.vAPI || {}; vAPI.firefox = true; vAPI.sessionId = String.fromCharCode(Date.now() % 26 + 97) + @@ -67,6 +74,30 @@ vAPI.shutdown = (function() { /******************************************************************************/ +(function() { + var hostname = location.hostname; + if ( !hostname ) { + return; + } + var filters = self.rpc({ + fnName: 'getScriptTagFilters', + url: location.href, + hostname: hostname + }); + if ( typeof filters !== 'string' || filters === '' ) { + return; + } + var reFilters = new RegExp(filters); + document.addEventListener('beforescriptexecute', function(ev) { + if ( reFilters.test(ev.target.textContent) ) { + ev.preventDefault(); + ev.stopPropagation(); + } + }); +})(); + +/******************************************************************************/ + vAPI.messaging = { channels: {}, pending: {}, @@ -254,6 +285,12 @@ MessagingChannel.prototype.send = function(message, callback) { MessagingChannel.prototype.sendTo = function(message, toTabId, toChannel, callback) { var messaging = vAPI.messaging; + if ( !messaging ) { + if ( typeof callback === 'function' ) { + callback(); + } + return; + } // 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 diff --git a/src/background.html b/src/background.html index 5852880a9..43863d28d 100644 --- a/src/background.html +++ b/src/background.html @@ -30,6 +30,7 @@ + diff --git a/src/js/contentscript-start.js b/src/js/contentscript-start.js index 6397a1e3b..b4a2b046a 100644 --- a/src/js/contentscript-start.js +++ b/src/js/contentscript-start.js @@ -128,15 +128,7 @@ var netFilters = function(details) { //console.debug('document.querySelectorAll("%s") = %o', text, document.querySelectorAll(text)); }; -var onBeforeScriptExecuteHandler = function(ev) { - if ( vAPI.reScriptTagRegex.test(ev.target.textContent) ) { - ev.preventDefault(); - ev.stopPropagation(); - } -}; - var filteringHandler = function(details) { - var value; var styleTagCount = vAPI.styles.length; vAPI.skipCosmeticFiltering = !details || details.skipCosmeticFiltering; @@ -147,11 +139,6 @@ var filteringHandler = function(details) { if ( details.netHide.length !== 0 ) { netFilters(details); } - value = details.scriptTagRegex; - if ( typeof value === 'string' && value.length !== 0 ) { - vAPI.reScriptTagRegex = new RegExp(value); - document.addEventListener('beforescriptexecute', onBeforeScriptExecuteHandler); - } // The port will never be used again at this point, disconnecting allows // the browser to flush this script from memory. } diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index 8a0afa89e..1a0c8730c 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -237,7 +237,7 @@ var FilterParser = function() { this.invalid = false; this.cosmetic = true; this.reParser = /^\s*([^#]*)(##|#@#)(.+)\s*$/; - this.reScriptSelectorParser = /^script:contains\(\/.+?\/\)$/; + this.reScriptContains = /^script:contains\(.+?\)$/; }; /******************************************************************************/ @@ -286,16 +286,20 @@ FilterParser.prototype.parse = function(s) { // Script tag filters: pre-process them so that can be used with minimal // overhead in the content script. - if ( - this.suffix.charAt(0) === 's' && - this.reScriptSelectorParser.test(this.suffix) - ) { + // Example: focus.de##script:contains(/uabInject/) + if ( this.suffix.charAt(0) === 's' && this.reScriptContains.test(this.suffix) ) { // Currently supported only as non-generic selector. if ( this.prefix.length === 0 ) { this.invalid = true; return this; } - this.suffix = 'script//:' + this.suffix.slice(17, -2).replace(/\\/g, '\\'); + var suffix = this.suffix; + this.suffix = 'script//:'; + if ( suffix.charAt(16) !== '/' || suffix.slice(-2) !== '/)' ) { + this.suffix += suffix.slice(16, -1).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } else { + this.suffix += suffix.slice(17, -2).replace(/\\/g, '\\'); + } } this.unhide = matches[2].charAt(1) === '@' ? 1 : 0; @@ -1257,7 +1261,6 @@ FilterContainer.prototype.retrieveDomainSelectors = function(request) { cosmeticHide: [], cosmeticDonthide: [], netHide: [], - scriptTagRegex: this.retrieveScriptTagRegex(domain, hostname), netCollapse: µb.userSettings.collapseBlocked };