diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index c3a4d7aec..3da3f27b5 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -58,10 +58,6 @@ window.addEventListener('webextFlavor', function() { vAPI.webextFlavor.soup.has('user_stylesheet'); }, { once: true }); -vAPI.insertCSS = function(tabId, details) { - return chrome.tabs.insertCSS(tabId, details, vAPI.resetLastError); -}; - const noopFunc = function(){}; /******************************************************************************/ @@ -364,6 +360,16 @@ vAPI.Tabs = class { }); } + async executeScript() { + let result; + try { + result = await webext.tabs.executeScript(...arguments); + } + catch(reason) { + } + return Array.isArray(result) ? result : []; + } + async get(tabId) { if ( tabId === null ) { return this.getCurrent(); @@ -383,6 +389,14 @@ vAPI.Tabs = class { return tabs.length !== 0 ? tabs[0] : null; } + async insertCSS() { + try { + await webext.tabs.insertCSS(...arguments); + } + catch(reason) { + } + } + async query(queryInfo) { let tabs; try { @@ -393,6 +407,14 @@ vAPI.Tabs = class { return Array.isArray(tabs) ? tabs : []; } + async removeCSS() { + try { + await webext.tabs.removeCSS(...arguments); + } + catch(reason) { + } + } + // Properties of the details object: // - url: 'URL', => the address that will be opened // - index: -1, => undefined: end of the list, -1: following tab, @@ -600,28 +622,6 @@ vAPI.Tabs = class { vAPI.windows.update(tab.windowId, { focused: true }); } - injectScript(tabId, details, callback) { - const onScriptExecuted = function() { - // https://code.google.com/p/chromium/issues/detail?id=410868#c8 - void browser.runtime.lastError; - if ( typeof callback === 'function' ) { - callback.apply(null, arguments); - } - }; - if ( tabId ) { - browser.tabs.executeScript( - toTabId(tabId), - details, - onScriptExecuted - ); - } else { - browser.tabs.executeScript( - details, - onScriptExecuted - ); - } - } - // https://forums.lanik.us/viewtopic.php?f=62&t=32826 // Chromium-based browsers: sanitize target URL. I've seen data: URI with // newline characters in standard fields, possibly as a way of evading @@ -984,29 +984,20 @@ vAPI.messaging = { if ( msg.add ) { details.runAt = 'document_start'; } - let countdown = 0; - const countdownHandler = function() { - void chrome.runtime.lastError; - countdown -= 1; - if ( countdown === 0 && typeof callback === 'function' ) { - callback(); - } - }; + const promises = []; for ( const cssText of msg.add ) { - countdown += 1; details.code = cssText; - browser.tabs.insertCSS(tabId, details, countdownHandler); + promises.push(vAPI.tabs.insertCSS(tabId, details)); } - if ( typeof chrome.tabs.removeCSS === 'function' ) { + if ( typeof webext.tabs.removeCSS === 'function' ) { for ( const cssText of msg.remove ) { - countdown += 1; details.code = cssText; - browser.tabs.removeCSS(tabId, details, countdownHandler); + promises.push(vAPI.tabs.removeCSS(tabId, details)); } } - if ( countdown === 0 && typeof callback === 'function' ) { + Promise.all(promises).then(( ) => { callback(); - } + }); break; } }, diff --git a/platform/chromium/webext.js b/platform/chromium/webext.js index 00b7da1fd..0712e5784 100644 --- a/platform/chromium/webext.js +++ b/platform/chromium/webext.js @@ -94,6 +94,22 @@ const webext = { // jshint ignore:line }); }); }, + executeScript: function() { + return new Promise(resolve => { + chrome.tabs.executeScript(...arguments, result => { + void chrome.runtime.lastError; + resolve(result); + }); + }); + }, + insertCSS: function() { + return new Promise(resolve => { + chrome.tabs.insertCSS(...arguments, ( ) => { + void chrome.runtime.lastError; + resolve(); + }); + }); + }, query: function() { return new Promise(resolve => { chrome.tabs.query(...arguments, tabs => { @@ -113,7 +129,7 @@ const webext = { // jshint ignore:line }, windows: { - get: async function() { + get: function() { return new Promise(resolve => { chrome.windows.get(...arguments, win => { void chrome.runtime.lastError; @@ -121,7 +137,7 @@ const webext = { // jshint ignore:line }); }); }, - create: async function() { + create: function() { return new Promise(resolve => { chrome.windows.create(...arguments, win => { void chrome.runtime.lastError; @@ -129,7 +145,7 @@ const webext = { // jshint ignore:line }); }); }, - update: async function() { + update: function() { return new Promise(resolve => { chrome.windows.update(...arguments, win => { void chrome.runtime.lastError; @@ -140,6 +156,18 @@ const webext = { // jshint ignore:line }, }; +// https://bugs.chromium.org/p/chromium/issues/detail?id=608854 +if ( chrome.tabs.removeCSS instanceof Function ) { + webext.tabs.removeCSS = function() { + return new Promise(resolve => { + chrome.tabs.removeCSS(...arguments, ( ) => { + void chrome.runtime.lastError; + resolve(); + }); + }); + }; +} + if ( chrome.storage.managed instanceof Object ) { webext.storage.managed = { get: function() { diff --git a/src/js/background.js b/src/js/background.js index 2f00d3446..47fa99ab7 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -65,8 +65,6 @@ const µBlock = (function() { // jshint ignore:line }; return { - firstInstall: false, - userSettings: { advancedUserEnabled: false, alwaysDetachLogger: true, diff --git a/src/js/cosmetic-filtering.js b/src/js/cosmetic-filtering.js index a4f044ef4..2a42e49c8 100644 --- a/src/js/cosmetic-filtering.js +++ b/src/js/cosmetic-filtering.js @@ -922,7 +922,7 @@ FilterContainer.prototype.retrieveGenericSelectors = function(request) { out.complex = []; } out.injected = injected.join(',\n'); - vAPI.insertCSS(request.tabId, { + vAPI.tabs.insertCSS(request.tabId, { code: out.injected + '\n{display:none!important;}', cssOrigin: 'user', frameId: request.frameId, @@ -1105,11 +1105,11 @@ FilterContainer.prototype.retrieveSpecificSelectors = function( }; if ( out.injectedHideFilters.length !== 0 ) { details.code = out.injectedHideFilters + '\n{display:none!important;}'; - vAPI.insertCSS(request.tabId, details); + vAPI.tabs.insertCSS(request.tabId, details); } if ( out.networkFilters.length !== 0 ) { details.code = out.networkFilters + '\n{display:none!important;}'; - vAPI.insertCSS(request.tabId, details); + vAPI.tabs.insertCSS(request.tabId, details); out.networkFilters = ''; } } diff --git a/src/js/messaging.js b/src/js/messaging.js index 0ccb7d123..3a4bd3b08 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -386,7 +386,7 @@ const onMessage = function(request, sender, callback) { if ( pageStore !== null ) { pageStore.hiddenElementCount = 0; pageStore.scriptCount = 0; - vAPI.tabs.injectScript(request.tabId, { + vAPI.tabs.executeScript(request.tabId, { allFrames: true, file: '/js/scriptlets/dom-survey.js', runAt: 'document_end' @@ -539,14 +539,11 @@ const onMessage = function(request, sender, callback) { if ( pageStore === null ) { break; } const fctxt = µb.filteringContext.fromTabId(tabId); if ( pageStore.filterScripting(fctxt, undefined) ) { - vAPI.tabs.injectScript( - tabId, - { - file: '/js/scriptlets/noscript-spoof.js', - frameId: frameId, - runAt: 'document_end' - } - ); + vAPI.tabs.executeScript(tabId, { + file: '/js/scriptlets/noscript-spoof.js', + frameId: frameId, + runAt: 'document_end' + }); } break; diff --git a/src/js/pagestore.js b/src/js/pagestore.js index cecb31eeb..64a94614e 100644 --- a/src/js/pagestore.js +++ b/src/js/pagestore.js @@ -394,7 +394,7 @@ const PageStore = class { } injectLargeMediaElementScriptlet() { - vAPI.tabs.injectScript(this.tabId, { + vAPI.tabs.executeScript(this.tabId, { file: '/js/scriptlets/load-large-media-interactive.js', allFrames: true, runAt: 'document_idle', diff --git a/src/js/scriptlet-filtering.js b/src/js/scriptlet-filtering.js index f2aebf6e3..1424076e4 100644 --- a/src/js/scriptlet-filtering.js +++ b/src/js/scriptlet-filtering.js @@ -449,15 +449,12 @@ if ( µb.hiddenSettings.debugScriptletInjector ) { code = 'debugger;\n' + code; } - vAPI.tabs.injectScript( - details.tabId, - { - code: code, - frameId: details.frameId, - matchAboutBlank: false, - runAt: 'document_start' - } - ); + vAPI.tabs.executeScript(details.tabId, { + code: code, + frameId: details.frameId, + matchAboutBlank: false, + runAt: 'document_start' + }); }; api.toSelfie = function() { diff --git a/src/js/start.js b/src/js/start.js index 05384814a..ff62e011b 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -48,87 +48,20 @@ vAPI.app.onShutdown = function() { /******************************************************************************/ -// Final initialization steps after all needed assets are in memory. -// - Initialize internal state with maybe already existing tabs. -// - Schedule next update operation. - -const onAllReady = function() { - µb.webRequest.start(); - - // Ensure that the resources allocated for decompression purpose (likely - // large buffers) are garbage-collectable immediately after launch. - // Otherwise I have observed that it may take quite a while before the - // garbage collection of these resources kicks in. Relinquishing as soon - // as possible ensure minimal memory usage baseline. - µb.lz4Codec.relinquish(); - - initializeTabs(); - - // https://github.com/chrisaljoudi/uBlock/issues/184 - // Check for updates not too far in the future. - µb.assets.addObserver(µb.assetObserver.bind(µb)); - µb.scheduleAssetUpdater( - µb.userSettings.autoUpdate - ? µb.hiddenSettings.autoUpdateDelayAfterLaunch * 1000 - : 0 - ); - - // vAPI.cloud is optional. - if ( µb.cloudStorageSupported ) { - vAPI.cloud.start([ - 'tpFiltersPane', - 'myFiltersPane', - 'myRulesPane', - 'whitelistPane' - ]); - } - - µb.contextMenu.update(null); - µb.firstInstall = false; - - // https://github.com/uBlockOrigin/uBlock-issues/issues/717 - // Prevent the extensions from being restarted mid-session. - browser.runtime.onUpdateAvailable.addListener(details => { - const toInt = vAPI.app.intFromVersion; - if ( - µBlock.hiddenSettings.extensionUpdateForceReload === true || - toInt(details.version) <= toInt(vAPI.app.version) - ) { - vAPI.app.restart(); - } - }); -}; - -/******************************************************************************/ - // This is called only once, when everything has been loaded in memory after // the extension was launched. It can be used to inject content scripts // in already opened web pages, to remove whatever nuisance could make it to // the web pages before uBlock was ready. const initializeTabs = async function() { - const handleScriptResponse = function(tabId, results) { - if ( - Array.isArray(results) === false || - results.length === 0 || - results[0] !== true - ) { - return; - } - // Inject dclarative content scripts programmatically. - const manifest = chrome.runtime.getManifest(); - if ( manifest instanceof Object === false ) { return; } - for ( const contentScript of manifest.content_scripts ) { - for ( const file of contentScript.js ) { - vAPI.tabs.injectScript(tabId, { - file: file, - allFrames: contentScript.all_frames, - runAt: contentScript.run_at - }); - } - } - }; + const manifest = browser.runtime.getManifest(); + if ( manifest instanceof Object === false ) { return; } + const tabs = await vAPI.tabs.query({ url: '' }); + const toCheck = []; + const checker = { + file: 'js/scriptlets/should-inject-contentscript.js' + }; for ( const tab of tabs ) { µb.tabContextManager.commit(tab.id, tab.url); µb.bindTabToPageStats(tab.id); @@ -136,13 +69,28 @@ const initializeTabs = async function() { // Find out whether content scripts need to be injected // programmatically. This may be necessary for web pages which // were loaded before uBO launched. - if ( /^https?:\/\//.test(tab.url) === false ) { continue; } - vAPI.tabs.injectScript( - tab.id, - { file: 'js/scriptlets/should-inject-contentscript.js' }, - handleScriptResponse.bind(null, tab.id) + toCheck.push( + /^https?:\/\//.test(tab.url) + ? vAPI.tabs.executeScript(tab.id, checker) + : false ); } + const results = await Promise.all(toCheck); + for ( let i = 0; i < results.length; i++ ) { + const result = results[i]; + if ( result.length === 0 || result[0] !== true ) { continue; } + // Inject dclarative content scripts programmatically. + const tabId = tabs[i].id; + for ( const contentScript of manifest.content_scripts ) { + for ( const file of contentScript.js ) { + vAPI.tabs.executeScript(tabId, { + file: file, + allFrames: contentScript.all_frames, + runAt: contentScript.run_at + }); + } + } + } }; /******************************************************************************/ @@ -250,9 +198,6 @@ const onFirstFetchReady = function(fetched) { fetched = createDefaultProps(); } - // https://github.com/gorhill/uBlock/issues/747 - µb.firstInstall = fetched.version === '0.0.0.0'; - // Order is important -- do not change: onSystemSettingsReady(fetched); fromFetch(µb.localSettings, fetched); @@ -350,7 +295,53 @@ try { console.trace(ex); } -onAllReady(); +// Final initialization steps after all needed assets are in memory. + +µb.webRequest.start(); + +// Ensure that the resources allocated for decompression purpose (likely +// large buffers) are garbage-collectable immediately after launch. +// Otherwise I have observed that it may take quite a while before the +// garbage collection of these resources kicks in. Relinquishing as soon +// as possible ensure minimal memory usage baseline. +µb.lz4Codec.relinquish(); + +// Initialize internal state with maybe already existing tabs. +initializeTabs(); + +// https://github.com/chrisaljoudi/uBlock/issues/184 +// Check for updates not too far in the future. +µb.assets.addObserver(µb.assetObserver.bind(µb)); +µb.scheduleAssetUpdater( + µb.userSettings.autoUpdate + ? µb.hiddenSettings.autoUpdateDelayAfterLaunch * 1000 + : 0 +); + +// vAPI.cloud is optional. +if ( µb.cloudStorageSupported ) { + vAPI.cloud.start([ + 'tpFiltersPane', + 'myFiltersPane', + 'myRulesPane', + 'whitelistPane' + ]); +} + +µb.contextMenu.update(null); + +// https://github.com/uBlockOrigin/uBlock-issues/issues/717 +// Prevent the extensions from being restarted mid-session. +browser.runtime.onUpdateAvailable.addListener(details => { + const toInt = vAPI.app.intFromVersion; + if ( + µBlock.hiddenSettings.extensionUpdateForceReload === true || + toInt(details.version) <= toInt(vAPI.app.version) + ) { + vAPI.app.restart(); + } +}); + log.info(`All ready ${Date.now()-vAPI.T0} ms after launch`); // <<<<< end of private scope diff --git a/src/js/storage.js b/src/js/storage.js index 6b7cc9647..437f76aae 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -255,7 +255,8 @@ return; } - // Select default filter lists if first-time launch. + // https://github.com/gorhill/uBlock/issues/747 + // Select default filter lists if first-time launch. const lists = await this.assets.metadata(); this.saveSelectedFilterLists(this.autoSelectRegionalFilterLists(lists)); }; diff --git a/src/js/ublock.js b/src/js/ublock.js index 21d2515db..e031aaed0 100644 --- a/src/js/ublock.js +++ b/src/js/ublock.js @@ -414,7 +414,7 @@ const matchBucket = function(url, hostname, bucket, start) { /******************************************************************************/ -µBlock.elementPickerExec = function(tabId, targetElement, zap) { +µBlock.elementPickerExec = async function(tabId, targetElement, zap) { if ( vAPI.isBehindTheSceneTabId(tabId) ) { return; } this.epickerTarget = targetElement || ''; @@ -422,27 +422,20 @@ const matchBucket = function(url, hostname, bucket, start) { // https://github.com/uBlockOrigin/uBlock-issues/issues/40 // The element picker needs this library - vAPI.tabs.injectScript( - tabId, - { - file: '/lib/diff/swatinem_diff.js', - runAt: 'document_end' - } - ); + vAPI.tabs.executeScript(tabId, { + file: '/lib/diff/swatinem_diff.js', + runAt: 'document_end' + }); + + await vAPI.tabs.executeScript(tabId, { + file: '/js/scriptlets/element-picker.js', + runAt: 'document_end' + }); // https://github.com/uBlockOrigin/uBlock-issues/issues/168 // Force activate the target tab once the element picker has been // injected. - vAPI.tabs.injectScript( - tabId, - { - file: '/js/scriptlets/element-picker.js', - runAt: 'document_end' - }, - ( ) => { - vAPI.tabs.select(tabId); - } - ); + vAPI.tabs.select(tabId); }; /******************************************************************************/ @@ -638,7 +631,7 @@ const matchBucket = function(url, hostname, bucket, start) { // cosmetic filters. µBlock.logCosmeticFilters = function(tabId, frameId) { - vAPI.tabs.injectScript(tabId, { + vAPI.tabs.executeScript(tabId, { file: '/js/scriptlets/cosmetic-logger.js', frameId: frameId, runAt: 'document_start' @@ -694,15 +687,15 @@ const matchBucket = function(url, hostname, bucket, start) { } pendingEntries.set(key, new Entry(tabId, scriptlet, callback)); } - vAPI.tabs.injectScript(tabId, { - file: '/js/scriptlets/' + scriptlet + '.js' + vAPI.tabs.executeScript(tabId, { + file: `/js/scriptlets/${scriptlet}.js` }); }; // TODO: think about a callback mechanism. const injectDeep = function(tabId, scriptlet) { - vAPI.tabs.injectScript(tabId, { - file: '/js/scriptlets/' + scriptlet + '.js', + vAPI.tabs.executeScript(tabId, { + file: `/js/scriptlets/${scriptlet}.js`, allFrames: true }); };