From e27328f9319132ba7d7a27de159aa077cf80ff37 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Sun, 15 Sep 2019 07:58:28 -0400 Subject: [PATCH] Work toward modernizing code base: promisification Swathes of code have been converted to use Promises/async/await. More left to do. In the process, a regression affecting the fix to has been fixed. --- platform/chromium/vapi-background.js | 143 ++++- src/js/assets.js | 919 +++++++++++---------------- src/js/messaging.js | 118 ++-- src/js/redirect-engine.js | 72 +-- src/js/reverselookup.js | 169 +++-- src/js/start.js | 104 ++- src/js/storage.js | 714 ++++++++++----------- 7 files changed, 1032 insertions(+), 1207 deletions(-) diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 6e21151de..a9234ee70 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -43,6 +43,15 @@ vAPI.lastError = function() { return chrome.runtime.lastError; }; +vAPI.apiIsPromisified = (( ) => { + try { + return browser.storage.local.get('_') instanceof Promise; + } + catch(ex) { + } + return false; +})(); + // https://github.com/gorhill/uBlock/issues/875 // https://code.google.com/p/chromium/issues/detail?id=410868#c8 // Must not leave `lastError` unchecked. @@ -107,9 +116,83 @@ vAPI.app = { /******************************************************************************/ /******************************************************************************/ -// chrome.storage.local.get(null, function(bin){ console.debug('%o', bin); }); - -vAPI.storage = browser.storage.local; +vAPI.storage = (( ) => { + if ( vAPI.apiIsPromisified ) { + return browser.storage.local; + } + return { + clear: function(callback) { + if ( callback !== undefined ) { + return browser.storage.local.clear(...arguments); + } + return new Promise((resolve, reject) => { + browser.storage.local.clear(( ) => { + const lastError = browser.runtime.lastError; + if ( lastError instanceof Object ) { + return reject(lastError); + } + resolve(); + }); + }); + }, + get: function(keys, callback) { + if ( callback !== undefined ) { + return browser.storage.local.get(...arguments); + } + return new Promise((resolve, reject) => { + browser.storage.local.get(keys, result => { + const lastError = browser.runtime.lastError; + if ( lastError instanceof Object ) { + return reject(lastError); + } + resolve(result); + }); + }); + }, + getBytesInUse: function(keys, callback) { + if ( callback !== undefined ) { + return browser.storage.local.getBytesInUse(...arguments); + } + return new Promise((resolve, reject) => { + browser.storage.local.getBytesInUse(keys, result => { + const lastError = browser.runtime.lastError; + if ( lastError instanceof Object ) { + return reject(lastError); + } + resolve(result); + }); + }); + }, + remove: function(keys, callback) { + if ( callback !== undefined ) { + return browser.storage.local.remove(...arguments); + } + return new Promise((resolve, reject) => { + browser.storage.local.remove(keys, ( ) => { + const lastError = browser.runtime.lastError; + if ( lastError instanceof Object ) { + return reject(lastError); + } + resolve(); + }); + }); + }, + set: function(items, callback) { + if ( callback !== undefined ) { + return browser.storage.local.set(...arguments); + } + return new Promise((resolve, reject) => { + browser.storage.local.set(items, ( ) => { + const lastError = browser.runtime.lastError; + if ( lastError instanceof Object ) { + return reject(lastError); + } + resolve(); + }); + }); + }, + }; +})(); /******************************************************************************/ /******************************************************************************/ @@ -1286,7 +1369,7 @@ vAPI.commands = chrome.commands; /******************************************************************************/ // https://github.com/gorhill/uBlock/issues/531 -// Storage area dedicated to admin settings. Read-only. +// Storage area dedicated to admin settings. Read-only. // https://github.com/gorhill/uBlock/commit/43a5ed735b95a575a9339b6e71a1fcb27a99663b#commitcomment-13965030 // Not all Chromium-based browsers support managed storage. Merely testing or @@ -1297,26 +1380,48 @@ vAPI.commands = chrome.commands; // https://github.com/gorhill/uBlock/issues/900 // Also, UC Browser: http://www.upsieutoc.com/image/WXuH -vAPI.adminStorage = chrome.storage.managed && { - getItem: function(key, callback) { - const onRead = function(store) { +vAPI.adminStorage = (( ) => { + if ( browser.storage.managed instanceof Object === false ) { + return { + getItem: function() { + return Promise.resolve(); + }, + }; + } + const managedStorage = vAPI.apiIsPromisified + ? browser.storage.managed + : { + get: function(keys) { + return new Promise((resolve, reject) => { + browser.storage.managed.get(keys, result => { + const lastError = browser.runtime.lastError; + if ( lastError instanceof Object ) { + return reject(lastError); + } + resolve(result); + }); + }); + }, + }; + return { + getItem: async function(key) { + let bin; + try { + bin = await managedStorage.get(key); + } catch(ex) { + } let data; if ( - !chrome.runtime.lastError && - typeof store === 'object' && - store !== null + chrome.runtime.lastError instanceof Object === false && + bin instanceof Object ) { - data = store[key]; + data = bin[key]; } - callback(data); - }; - try { - chrome.storage.managed.get(key, onRead); - } catch (ex) { - callback(); + return data; } - } -}; + }; +})(); + /******************************************************************************/ /******************************************************************************/ diff --git a/src/js/assets.js b/src/js/assets.js index 8e39f41fb..fce3b3463 100644 --- a/src/js/assets.js +++ b/src/js/assets.js @@ -27,10 +27,9 @@ /******************************************************************************/ -const reIsExternalPath = /^(?:[a-z-]+):\/\//, - reIsUserAsset = /^user-/, - errorCantConnectTo = vAPI.i18n('errorCantConnectTo'), - noopfunc = function(){}; +const reIsExternalPath = /^(?:[a-z-]+):\/\//; +const reIsUserAsset = /^user-/; +const errorCantConnectTo = vAPI.i18n('errorCantConnectTo'); const api = {}; @@ -82,18 +81,28 @@ api.fetch = function(url, options = {}) { } }; + const fail = function(details, msg) { + µBlock.logger.writeOne({ + realm: 'message', + type: 'error', + text: msg, + }); + details.content = ''; + details.error = msg; + reject(details); + }; + // https://github.com/gorhill/uMatrix/issues/15 const onLoadEvent = function() { cleanup(); // xhr for local files gives status 0, but actually succeeds const details = { url, - content: '', statusCode: this.status || 200, statusText: this.statusText || '' }; if ( details.statusCode < 200 || details.statusCode >= 300 ) { - return reject(details); + return fail(details, `${url}: ${details.statusCode} ${details.statusText}`); } details.content = this.response; resolve(details); @@ -101,12 +110,7 @@ api.fetch = function(url, options = {}) { const onErrorEvent = function() { cleanup(); - µBlock.logger.writeOne({ - realm: 'message', - type: 'error', - text: errorCantConnectTo.replace('{{msg}}', url) - }); - reject({ url, content: '' }); + fail({ url }, errorCantConnectTo.replace('{{msg}}', url)); }; const onTimeout = function() { @@ -146,7 +150,7 @@ api.fetch = function(url, options = {}) { /******************************************************************************/ -api.fetchText = function(url, onLoad, onError) { +api.fetchText = async function(url) { const isExternal = reIsExternalPath.test(url); let actualUrl = isExternal ? url : vAPI.getURL(url); @@ -171,41 +175,31 @@ api.fetchText = function(url, onLoad, onError) { actualUrl += queryValue; } - if ( typeof onError !== 'function' ) { - onError = onLoad; - } + let details = { content: '' }; + try { + details = await api.fetch(actualUrl); - const onResolve = function(details) { - if ( onLoad instanceof Function ) { - return onLoad(details); - } - return details; - }; - - const onReject = function(details) { - details.content = ''; - if ( onError instanceof Function ) { - return onError(details); - } - return details; - }; - - return api.fetch(url).then(details => { - // consider an empty result to be an error + // Consider an empty result to be an error if ( stringIsNotEmpty(details.content) === false ) { - return onReject(details); + details.content = ''; } - // we never download anything else than plain text: discard if response - // appears to be a HTML document: could happen when server serves - // some kind of error page I suppose + + // We never download anything else than plain text: discard if + // response appears to be a HTML document: could happen when server + // serves some kind of error page for example. const text = details.content.trim(); if ( text.startsWith('<') && text.endsWith('>') ) { - return onReject(details); + details.content = ''; } - return onResolve(details); - }).catch(details => { - return onReject(details); - }); + } catch(ex) { + details = ex; + } + + // We want to return the caller's URL, not our internal one which may + // differ from the caller's one. + details.url = url; + + return details; }; /******************************************************************************/ @@ -213,116 +207,83 @@ api.fetchText = function(url, onLoad, onError) { // https://github.com/gorhill/uBlock/issues/3331 // Support the seamless loading of sublists. -api.fetchFilterList = function(mainlistURL, onLoad, onError) { - const content = []; - const pendingSublistURLs = new Set([ mainlistURL ]); - const loadedSublistURLs = new Set(); - const toParsedURL = api.fetchFilterList.toParsedURL; +api.fetchFilterList = async function(mainlistURL) { + const toParsedURL = url => { + try { + return new URL(url); + } catch (ex) { + } + }; // https://github.com/NanoAdblocker/NanoCore/issues/239 // Anything under URL's root directory is allowed to be fetched. The // URL of a sublist will always be relative to the URL of the parent // list (instead of the URL of the root list). - const rootDirectoryURL = toParsedURL(mainlistURL); + let rootDirectoryURL = toParsedURL(mainlistURL); if ( rootDirectoryURL !== undefined ) { const pos = rootDirectoryURL.pathname.lastIndexOf('/'); if ( pos !== -1 ) { rootDirectoryURL.pathname = rootDirectoryURL.pathname.slice(0, pos + 1); + } else { + rootDirectoryURL = undefined; } } - let errored = false; + const sublistURLs = new Set(); - const processIncludeDirectives = function(details) { - const reInclude = /^!#include +(\S+)/gm; + const processIncludeDirectives = function(results) { const out = []; - const content = details.content; - let lastIndex = 0; - for (;;) { - const match = reInclude.exec(content); - if ( match === null ) { break; } - if ( toParsedURL(match[1]) !== undefined ) { continue; } - if ( match[1].indexOf('..') !== -1 ) { continue; } - const subURL = toParsedURL(details.url); - subURL.pathname = subURL.pathname.replace(/[^/]+$/, match[1]); - if ( subURL.href.startsWith(rootDirectoryURL.href) === false ) { + const reInclude = /^!#include +(\S+)/gm; + for ( const result of results ) { + if ( result instanceof Object === false ) { + out.push(result); continue; } - if ( pendingSublistURLs.has(subURL.href) ) { continue; } - if ( loadedSublistURLs.has(subURL.href) ) { continue; } - pendingSublistURLs.add(subURL.href); - api.fetchText(subURL.href, onLocalLoadSuccess, onLocalLoadError); - out.push(content.slice(lastIndex, match.index).trim(), subURL.href); - lastIndex = reInclude.lastIndex; - } - out.push(lastIndex === 0 ? content : content.slice(lastIndex).trim()); - return out; - }; - - const onLocalLoadSuccess = function(details) { - if ( errored ) { return; } - - const isSublist = details.url !== mainlistURL; - - pendingSublistURLs.delete(details.url); - loadedSublistURLs.add(details.url); - - // https://github.com/uBlockOrigin/uBlock-issues/issues/329 - // Insert fetched content at position of related #!include directive - let slot = isSublist ? content.indexOf(details.url) : 0; - if ( isSublist ) { - content.splice( - slot, - 1, - '! >>>>>>>> ' + details.url, - details.content.trim(), - '! <<<<<<<< ' + details.url - ); - slot += 1; - } else { - content[0] = details.content.trim(); - } - - // Find and process #!include directives - if ( - rootDirectoryURL !== undefined && - rootDirectoryURL.pathname.length > 0 - ) { - const processed = processIncludeDirectives(details); - if ( processed.length > 1 ) { - content.splice(slot, 1, ...processed); + const content = result.content; + let lastIndex = 0; + for (;;) { + if ( rootDirectoryURL === undefined ) { break; } + const match = reInclude.exec(content); + if ( match === null ) { break; } + if ( toParsedURL(match[1]) !== undefined ) { continue; } + if ( match[1].indexOf('..') !== -1 ) { continue; } + const subURL = toParsedURL(result.url); + subURL.pathname = subURL.pathname.replace(/[^/]+$/, match[1]); + if ( subURL.href.startsWith(rootDirectoryURL.href) === false ) { + continue; + } + if ( sublistURLs.has(subURL.href) ) { continue; } + sublistURLs.add(subURL.href); + out.push( + content.slice(lastIndex, match.index).trim(), + `\n! >>>>>>>> ${subURL.href}\n`, + api.fetchText(subURL.href), + `! <<<<<<<< ${subURL.href}\n` + ); + lastIndex = reInclude.lastIndex; } + out.push(lastIndex === 0 ? content : content.slice(lastIndex).trim()); } - - if ( pendingSublistURLs.size !== 0 ) { return; } - - details.url = mainlistURL; - details.content = content.join('\n').trim(); - onLoad(details); + return out; }; // https://github.com/AdguardTeam/FiltersRegistry/issues/82 // Not checking for `errored` status was causing repeated notifications // to the caller. This can happen when more than one out of multiple // sublists can't be fetched. - const onLocalLoadError = function(details) { - if ( errored ) { return; } - errored = true; - details.url = mainlistURL; - details.content = ''; - onError(details); - }; - - this.fetchText(mainlistURL, onLocalLoadSuccess, onLocalLoadError); -}; - -api.fetchFilterList.toParsedURL = function(url) { - try { - return new URL(url); - } catch (ex) { + let allParts = [ + this.fetchText(mainlistURL) + ]; + for (;;) { + allParts = processIncludeDirectives(await Promise.all(allParts)); + if ( allParts.every(v => typeof v === 'string') ) { break; } } + return { + url: mainlistURL, + content: allParts.join('') + }; }; /******************************************************************************* @@ -341,6 +302,34 @@ api.fetchFilterList.toParsedURL = function(url) { let assetSourceRegistryPromise, assetSourceRegistry = Object.create(null); +const getAssetSourceRegistry = function() { + if ( assetSourceRegistryPromise === undefined ) { + assetSourceRegistryPromise = µBlock.cacheStorage.get( + 'assetSourceRegistry' + ).then(bin => { + if ( + bin instanceof Object && + bin.assetSourceRegistry instanceof Object + ) { + assetSourceRegistry = bin.assetSourceRegistry; + return assetSourceRegistry; + } + return api.fetchText( + µBlock.assetsBootstrapLocation || 'assets/assets.json' + ).then(details => { + return details.content !== '' + ? details + : api.fetchText('assets/assets.json'); + }).then(details => { + updateAssetSourceRegistry(details.content, true); + return assetSourceRegistry; + }); + }); + } + + return assetSourceRegistryPromise; +}; + const registerAssetSource = function(assetKey, dict) { const entry = assetSourceRegistry[assetKey] || {}; for ( const prop in dict ) { @@ -383,7 +372,7 @@ const unregisterAssetSource = function(assetKey) { delete assetSourceRegistry[assetKey]; }; -const saveAssetSourceRegistry = (function() { +const saveAssetSourceRegistry = (( ) => { let timer; const save = function() { timer = undefined; @@ -433,46 +422,16 @@ const updateAssetSourceRegistry = function(json, silent) { saveAssetSourceRegistry(); }; -const getAssetSourceRegistry = function() { - if ( assetSourceRegistryPromise === undefined ) { - assetSourceRegistryPromise = µBlock.cacheStorage.get( - 'assetSourceRegistry' - ).then(bin => { - if ( - bin instanceof Object && - bin.assetSourceRegistry instanceof Object - ) { - assetSourceRegistry = bin.assetSourceRegistry; - return assetSourceRegistry; - } - return api.fetchText( - µBlock.assetsBootstrapLocation || 'assets/assets.json' - ).then(details => { - return details.content !== '' - ? details - : api.fetchText('assets/assets.json'); - }).then(details => { - updateAssetSourceRegistry(details.content, true); - return assetSourceRegistry; - }); - }); - } - - return assetSourceRegistryPromise; +api.registerAssetSource = async function(assetKey, details) { + await getAssetSourceRegistry(); + registerAssetSource(assetKey, details); + saveAssetSourceRegistry(true); }; -api.registerAssetSource = function(assetKey, details) { - getAssetSourceRegistry().then(( ) => { - registerAssetSource(assetKey, details); - saveAssetSourceRegistry(true); - }); -}; - -api.unregisterAssetSource = function(assetKey) { - getAssetSourceRegistry().then(( ) => { - unregisterAssetSource(assetKey); - saveAssetSourceRegistry(true); - }); +api.unregisterAssetSource = async function(assetKey) { + await getAssetSourceRegistry(); + unregisterAssetSource(assetKey); + saveAssetSourceRegistry(true); }; /******************************************************************************* @@ -497,13 +456,14 @@ const getAssetCacheRegistry = function() { ) { assetCacheRegistry = bin.assetCacheRegistry; } + return assetCacheRegistry; }); } - return assetCacheRegistryPromise.then(( ) => assetCacheRegistry); + return assetCacheRegistryPromise; }; -const saveAssetCacheRegistry = (function() { +const saveAssetCacheRegistry = (( ) => { let timer; const save = function() { timer = undefined; @@ -519,42 +479,38 @@ const saveAssetCacheRegistry = (function() { }; })(); -const assetCacheRead = function(assetKey, callback) { - const internalKey = 'cache/' + assetKey; +const assetCacheRead = async function(assetKey) { + const internalKey = `cache/${assetKey}`; const reportBack = function(content) { if ( content instanceof Blob ) { content = ''; } let details = { assetKey: assetKey, content: content }; - if ( content === '' ) { details.error = 'E_NOTFOUND'; } - callback(details); + if ( content === '' ) { details.error = 'ENOTFOUND'; } + return details; }; - const onAssetRead = function(bin) { - if ( - bin instanceof Object === false || - bin.hasOwnProperty(internalKey) === false - ) { - return reportBack(''); - } - let entry = assetCacheRegistry[assetKey]; - if ( entry === undefined ) { - return reportBack(''); - } - entry.readTime = Date.now(); - saveAssetCacheRegistry(true); - reportBack(bin[internalKey]); - }; - - Promise.all([ + const [ , bin ] = await Promise.all([ getAssetCacheRegistry(), µBlock.cacheStorage.get(internalKey), - ]).then(results => { - onAssetRead(results[1]); - }); + ]); + if ( + bin instanceof Object === false || + bin.hasOwnProperty(internalKey) === false + ) { + return reportBack(''); + } + + const entry = assetCacheRegistry[assetKey]; + if ( entry === undefined ) { + return reportBack(''); + } + + entry.readTime = Date.now(); + saveAssetCacheRegistry(true); + return reportBack(bin[internalKey]); }; -const assetCacheWrite = function(assetKey, details, callback) { - let internalKey = 'cache/' + assetKey; +const assetCacheWrite = async function(assetKey, details) { let content = ''; if ( typeof details === 'string' ) { content = details; @@ -563,95 +519,83 @@ const assetCacheWrite = function(assetKey, details, callback) { } if ( content === '' ) { - return assetCacheRemove(assetKey, callback); + return assetCacheRemove(assetKey); } - const onReady = function() { - let entry = assetCacheRegistry[assetKey]; - if ( entry === undefined ) { - entry = assetCacheRegistry[assetKey] = {}; - } - entry.writeTime = entry.readTime = Date.now(); - if ( details instanceof Object && typeof details.url === 'string' ) { - entry.remoteURL = details.url; - } - µBlock.cacheStorage.set({ assetCacheRegistry, [internalKey]: content }); - const result = { assetKey, content }; - if ( typeof callback === 'function' ) { - callback(result); - } - // https://github.com/uBlockOrigin/uBlock-issues/issues/248 - fireNotification('after-asset-updated', result); - }; + const cacheDict = await getAssetCacheRegistry(); - getAssetCacheRegistry().then(( ) => onReady()); -}; - -const assetCacheRemove = function(pattern, callback) { - getAssetCacheRegistry().then(cacheDict => { - const removedEntries = []; - const removedContent = []; - for ( const assetKey in cacheDict ) { - if ( pattern instanceof RegExp && !pattern.test(assetKey) ) { - continue; - } - if ( typeof pattern === 'string' && assetKey !== pattern ) { - continue; - } - removedEntries.push(assetKey); - removedContent.push('cache/' + assetKey); - delete cacheDict[assetKey]; - } - if ( removedContent.length !== 0 ) { - µBlock.cacheStorage.remove(removedContent); - µBlock.cacheStorage.set({ assetCacheRegistry }); - } - if ( typeof callback === 'function' ) { - callback(); - } - for ( let i = 0; i < removedEntries.length; i++ ) { - fireNotification( - 'after-asset-updated', - { assetKey: removedEntries[i] } - ); - } - }); -}; - -const assetCacheMarkAsDirty = function(pattern, exclude, callback) { - if ( typeof exclude === 'function' ) { - callback = exclude; - exclude = undefined; + let entry = cacheDict[assetKey]; + if ( entry === undefined ) { + entry = cacheDict[assetKey] = {}; } - getAssetCacheRegistry().then(cacheDict => { - let mustSave = false; - for ( const assetKey in cacheDict ) { - if ( pattern instanceof RegExp ) { - if ( pattern.test(assetKey) === false ) { continue; } - } else if ( typeof pattern === 'string' ) { - if ( assetKey !== pattern ) { continue; } - } else if ( Array.isArray(pattern) ) { - if ( pattern.indexOf(assetKey) === -1 ) { continue; } - } - if ( exclude instanceof RegExp ) { - if ( exclude.test(assetKey) ) { continue; } - } else if ( typeof exclude === 'string' ) { - if ( assetKey === exclude ) { continue; } - } else if ( Array.isArray(exclude) ) { - if ( exclude.indexOf(assetKey) !== -1 ) { continue; } - } - const cacheEntry = cacheDict[assetKey]; - if ( !cacheEntry.writeTime ) { continue; } - cacheDict[assetKey].writeTime = 0; - mustSave = true; - } - if ( mustSave ) { - µBlock.cacheStorage.set({ assetCacheRegistry }); - } - if ( typeof callback === 'function' ) { - callback(); - } + entry.writeTime = entry.readTime = Date.now(); + if ( details instanceof Object && typeof details.url === 'string' ) { + entry.remoteURL = details.url; + } + µBlock.cacheStorage.set({ + cacheDict, + [`cache/${assetKey}`]: content }); + + const result = { assetKey, content }; + // https://github.com/uBlockOrigin/uBlock-issues/issues/248 + fireNotification('after-asset-updated', result); + return result; +}; + +const assetCacheRemove = async function(pattern) { + const cacheDict = await getAssetCacheRegistry(); + const removedEntries = []; + const removedContent = []; + for ( const assetKey in cacheDict ) { + if ( pattern instanceof RegExp && !pattern.test(assetKey) ) { + continue; + } + if ( typeof pattern === 'string' && assetKey !== pattern ) { + continue; + } + removedEntries.push(assetKey); + removedContent.push('cache/' + assetKey); + delete cacheDict[assetKey]; + } + if ( removedContent.length !== 0 ) { + µBlock.cacheStorage.remove(removedContent); + µBlock.cacheStorage.set({ assetCacheRegistry }); + } + for ( let i = 0; i < removedEntries.length; i++ ) { + fireNotification( + 'after-asset-updated', + { assetKey: removedEntries[i] } + ); + } +}; + +const assetCacheMarkAsDirty = async function(pattern, exclude) { + const cacheDict = await getAssetCacheRegistry(); + let mustSave = false; + for ( const assetKey in cacheDict ) { + if ( pattern instanceof RegExp ) { + if ( pattern.test(assetKey) === false ) { continue; } + } else if ( typeof pattern === 'string' ) { + if ( assetKey !== pattern ) { continue; } + } else if ( Array.isArray(pattern) ) { + if ( pattern.indexOf(assetKey) === -1 ) { continue; } + } + if ( exclude instanceof RegExp ) { + if ( exclude.test(assetKey) ) { continue; } + } else if ( typeof exclude === 'string' ) { + if ( assetKey === exclude ) { continue; } + } else if ( Array.isArray(exclude) ) { + if ( exclude.indexOf(assetKey) !== -1 ) { continue; } + } + const cacheEntry = cacheDict[assetKey]; + if ( !cacheEntry.writeTime ) { continue; } + cacheDict[assetKey].writeTime = 0; + mustSave = true; + } + if ( mustSave ) { + µBlock.cacheStorage.set({ assetCacheRegistry }); + } }; /******************************************************************************/ @@ -678,141 +622,89 @@ const stringIsNotEmpty = function(s) { **/ -const readUserAsset = function(assetKey, callback) { - const reportBack = function(content) { - callback({ assetKey, content }); - }; - vAPI.storage.get(assetKey, bin => { - const content = - bin instanceof Object && typeof bin[assetKey] === 'string' - ? bin[assetKey] - : ''; - return reportBack(content); - }); +const readUserAsset = async function(assetKey) { + const bin = await vAPI.storage.get(assetKey); + const content = + bin instanceof Object && typeof bin[assetKey] === 'string' + ? bin[assetKey] + : ''; + // Remove obsolete entry // TODO: remove once everybody is well beyond 1.18.6 vAPI.storage.remove('assets/user/filters.txt'); + + return { assetKey, content }; }; -const saveUserAsset = function(assetKey, content, callback) { - vAPI.storage.set({ [assetKey]: content }, ( ) => { - if ( callback instanceof Function ) { - callback({ assetKey, content }); - } +const saveUserAsset = function(assetKey, content) { + return vAPI.storage.set({ [assetKey]: content }).then(( ) => { + return { assetKey, content }; }); }; /******************************************************************************/ -api.get = function(assetKey, options, callback) { - if ( typeof options === 'function' ) { - callback = options; - options = {}; - } else if ( typeof callback !== 'function' ) { - callback = noopfunc; - } - // This can happen if the method was called as a thenable. - if ( options instanceof Object === false ) { - options = {}; - } - - return new Promise(resolve => { - // start of executor +api.get = async function(assetKey, options = {}) { if ( assetKey === µBlock.userFiltersPath ) { - readUserAsset(assetKey, details => { - callback(details); - resolve(details); - }); - return; + return readUserAsset(assetKey); } - let assetDetails = {}, - contentURLs, - contentURL; + let assetDetails = {}; - const reportBack = (content, err) => { + const reportBack = (content, url = '', err = undefined) => { const details = { assetKey, content }; - if ( err ) { + if ( err !== undefined ) { details.error = assetDetails.lastError = err; } else { assetDetails.lastError = undefined; } if ( options.needSourceURL ) { if ( - contentURL === undefined && + url === '' && assetCacheRegistry instanceof Object && assetCacheRegistry[assetKey] instanceof Object ) { details.sourceURL = assetCacheRegistry[assetKey].remoteURL; } - if ( reIsExternalPath.test(contentURL) ) { - details.sourceURL = contentURL; + if ( reIsExternalPath.test(url) ) { + details.sourceURL = url; } } - callback(details); - resolve(details); + return details; }; - const onContentNotLoaded = ( ) => { - let isExternal; - while ( (contentURL = contentURLs.shift()) ) { - isExternal = reIsExternalPath.test(contentURL); - if ( isExternal === false || assetDetails.hasLocalURL !== true ) { - break; - } - } - if ( !contentURL ) { - return reportBack('', 'E_NOTFOUND'); - } - if ( assetDetails.content === 'filters' ) { - api.fetchFilterList(contentURL, onContentLoaded, onContentNotLoaded); - } else { - api.fetchText(contentURL, onContentLoaded, onContentNotLoaded); - } - }; + const details = await assetCacheRead(assetKey); + if ( details.content !== '' ) { + return reportBack(details.content); + } - const onContentLoaded = details => { - if ( stringIsNotEmpty(details.content) === false ) { - onContentNotLoaded(); - return; - } - if ( reIsExternalPath.test(contentURL) && options.dontCache !== true ) { - assetCacheWrite(assetKey, { - content: details.content, - url: contentURL - }); - } - reportBack(details.content); - }; + const assetRegistry = await getAssetSourceRegistry(); + assetDetails = assetRegistry[assetKey] || {}; + let contentURLs = []; + if ( typeof assetDetails.contentURL === 'string' ) { + contentURLs = [ assetDetails.contentURL ]; + } else if ( Array.isArray(assetDetails.contentURL) ) { + contentURLs = assetDetails.contentURL.slice(0); + } - const onCachedContentLoaded = details => { - if ( details.content !== '' ) { - return reportBack(details.content); + for ( const contentURL of contentURLs ) { + if ( reIsExternalPath.test(contentURL) && assetDetails.hasLocalURL ) { + continue; } - getAssetSourceRegistry().then(registry => { - assetDetails = registry[assetKey] || {}; - if ( typeof assetDetails.contentURL === 'string' ) { - contentURLs = [ assetDetails.contentURL ]; - } else if ( Array.isArray(assetDetails.contentURL) ) { - contentURLs = assetDetails.contentURL.slice(0); - } else { - contentURLs = []; - } - onContentNotLoaded(); - }); - }; - - assetCacheRead(assetKey, onCachedContentLoaded); - // end of executor - }); + const details = assetDetails.content === 'filters' + ? await api.fetchFilterList(contentURL) + : await api.fetchText(contentURL); + if ( details.content === '' ) { continue; } + return reportBack(details.content, contentURL); + } + return reportBack('', '', 'ENOTFOUND'); }; /******************************************************************************/ -const getRemote = function(assetKey, callback) { - let assetDetails = {}; - let contentURLs; - let contentURL; +const getRemote = async function(assetKey) { + const assetRegistry = await getAssetSourceRegistry(); + const assetDetails = assetRegistry[assetKey] || {}; const reportBack = function(content, err) { const details = { assetKey: assetKey, content: content }; @@ -821,123 +713,99 @@ const getRemote = function(assetKey, callback) { } else { assetDetails.lastError = undefined; } - callback(details); + return details; }; - const onRemoteContentLoaded = function(details) { - if ( stringIsNotEmpty(details.content) === false ) { - registerAssetSource(assetKey, { error: { time: Date.now(), error: 'No content' } }); - tryLoading(); - return; + let contentURLs = []; + if ( typeof assetDetails.contentURL === 'string' ) { + contentURLs = [ assetDetails.contentURL ]; + } else if ( Array.isArray(assetDetails.contentURL) ) { + contentURLs = assetDetails.contentURL.slice(0); + } + + for ( const contentURL of contentURLs ) { + if ( reIsExternalPath.test(contentURL) === false ) { continue; } + + const result = assetDetails.content === 'filters' + ? await api.fetchFilterList(contentURL) + : await api.fetchText(contentURL); + + // Failure + if ( stringIsNotEmpty(result.content) === false ) { + let error = result.statusText; + if ( result.statusCode === 0 ) { + error = 'network error'; + } + registerAssetSource( + assetKey, + { error: { time: Date.now(), error } } + ); + continue; } - assetCacheWrite(assetKey, { - content: details.content, - url: contentURL - }); + + // Success + assetCacheWrite( + assetKey, + { content: result.content, url: contentURL } + ); registerAssetSource(assetKey, { error: undefined }); - reportBack(details.content); - }; + return reportBack(result.content); + } - const onRemoteContentError = function(details) { - let text = details.statusText; - if ( details.statusCode === 0 ) { - text = 'network error'; - } - registerAssetSource(assetKey, { error: { time: Date.now(), error: text } }); - tryLoading(); - }; - - const tryLoading = function() { - while ( (contentURL = contentURLs.shift()) ) { - if ( reIsExternalPath.test(contentURL) ) { break; } - } - if ( !contentURL ) { - return reportBack('', 'E_NOTFOUND'); - } - if ( assetDetails.content === 'filters' ) { - api.fetchFilterList(contentURL, onRemoteContentLoaded, onRemoteContentError); - } else { - api.fetchText(contentURL, onRemoteContentLoaded, onRemoteContentError); - } - }; - - getAssetSourceRegistry().then(registry => { - assetDetails = registry[assetKey] || {}; - if ( typeof assetDetails.contentURL === 'string' ) { - contentURLs = [ assetDetails.contentURL ]; - } else if ( Array.isArray(assetDetails.contentURL) ) { - contentURLs = assetDetails.contentURL.slice(0); - } else { - contentURLs = []; - } - tryLoading(); - }); + return reportBack('', 'ENOTFOUND'); }; /******************************************************************************/ -api.put = function(assetKey, content, callback) { - return new Promise(resolve => { - const onDone = function(details) { - if ( typeof callback === 'function' ) { - callback(details); - } - resolve(details); - }; - if ( reIsUserAsset.test(assetKey) ) { - saveUserAsset(assetKey, content, onDone); - } else { - assetCacheWrite(assetKey, content, onDone); - } - }); +api.put = async function(assetKey, content) { + return reIsUserAsset.test(assetKey) + ? await saveUserAsset(assetKey, content) + : await assetCacheWrite(assetKey, content); }; /******************************************************************************/ -api.metadata = function(callback) { - const onReady = function() { - const assetDict = JSON.parse(JSON.stringify(assetSourceRegistry)); - const cacheDict = assetCacheRegistry; - const now = Date.now(); - for ( const assetKey in assetDict ) { - const assetEntry = assetDict[assetKey]; - const cacheEntry = cacheDict[assetKey]; - if ( cacheEntry ) { - assetEntry.cached = true; - assetEntry.writeTime = cacheEntry.writeTime; - const obsoleteAfter = - cacheEntry.writeTime + assetEntry.updateAfter * 86400000; - assetEntry.obsolete = obsoleteAfter < now; - assetEntry.remoteURL = cacheEntry.remoteURL; - } else if ( - assetEntry.contentURL && - assetEntry.contentURL.length !== 0 - ) { - assetEntry.writeTime = 0; - assetEntry.obsolete = true; - } - } - callback(assetDict); - }; - - Promise.all([ +api.metadata = async function() { + await Promise.all([ getAssetSourceRegistry(), getAssetCacheRegistry(), - ]).then( - ( ) => onReady() - ); + ]); + + const assetDict = JSON.parse(JSON.stringify(assetSourceRegistry)); + const cacheDict = assetCacheRegistry; + const now = Date.now(); + for ( const assetKey in assetDict ) { + const assetEntry = assetDict[assetKey]; + const cacheEntry = cacheDict[assetKey]; + if ( cacheEntry ) { + assetEntry.cached = true; + assetEntry.writeTime = cacheEntry.writeTime; + const obsoleteAfter = + cacheEntry.writeTime + assetEntry.updateAfter * 86400000; + assetEntry.obsolete = obsoleteAfter < now; + assetEntry.remoteURL = cacheEntry.remoteURL; + } else if ( + assetEntry.contentURL && + assetEntry.contentURL.length !== 0 + ) { + assetEntry.writeTime = 0; + assetEntry.obsolete = true; + } + } + + return assetDict; }; /******************************************************************************/ api.purge = assetCacheMarkAsDirty; -api.remove = function(pattern, callback) { - assetCacheRemove(pattern, callback); +api.remove = function(pattern) { + return assetCacheRemove(pattern); }; api.rmrf = function() { - assetCacheRemove(/./); + return assetCacheRemove(/./); }; /******************************************************************************/ @@ -959,75 +827,58 @@ const updateFirst = function() { updateNext(); }; -const updateNext = function() { - let assetDict, cacheDict; - - // This will remove a cached asset when it's no longer in use. - const garbageCollectOne = function(assetKey) { - const cacheEntry = cacheDict[assetKey]; - if ( cacheEntry && cacheEntry.readTime < assetCacheRegistryStartTime ) { - assetCacheRemove(assetKey); - } - }; - - const findOne = function() { - const now = Date.now(); - for ( const assetKey in assetDict ) { - const assetEntry = assetDict[assetKey]; - if ( assetEntry.hasRemoteURL !== true ) { continue; } - if ( updaterFetched.has(assetKey) ) { continue; } - const cacheEntry = cacheDict[assetKey]; - if ( - cacheEntry && - (cacheEntry.writeTime + assetEntry.updateAfter * 86400000) > now - ) { - continue; - } - if ( - fireNotification( - 'before-asset-updated', - { assetKey: assetKey, type: assetEntry.content } - ) === true - ) { - return assetKey; - } - garbageCollectOne(assetKey); - } - }; - - const updatedOne = function(details) { - if ( details.content !== '' ) { - updaterUpdated.push(details.assetKey); - if ( details.assetKey === 'assets.json' ) { - updateAssetSourceRegistry(details.content); - } - } else { - fireNotification('asset-update-failed', { assetKey: details.assetKey }); - } - if ( findOne() !== undefined ) { - vAPI.setTimeout(updateNext, updaterAssetDelay); - } else { - updateDone(); - } - }; - - const updateOne = function() { - const assetKey = findOne(); - if ( assetKey === undefined ) { - return updateDone(); - } - updaterFetched.add(assetKey); - getRemote(assetKey, updatedOne); - }; - - Promise.all([ +const updateNext = async function() { + const [ assetDict, cacheDict ] = await Promise.all([ getAssetSourceRegistry(), getAssetCacheRegistry(), - ]).then(results => { - assetDict = results[0]; - cacheDict = results[1]; - updateOne(); - }); + ]); + + const now = Date.now(); + let assetKeyToUpdate; + for ( const assetKey in assetDict ) { + const assetEntry = assetDict[assetKey]; + if ( assetEntry.hasRemoteURL !== true ) { continue; } + if ( updaterFetched.has(assetKey) ) { continue; } + const cacheEntry = cacheDict[assetKey]; + if ( + cacheEntry && + (cacheEntry.writeTime + assetEntry.updateAfter * 86400000) > now + ) { + continue; + } + if ( + fireNotification( + 'before-asset-updated', + { assetKey: assetKey, type: assetEntry.content } + ) === true + ) { + assetKeyToUpdate = assetKey; + break; + } + // This will remove a cached asset when it's no longer in use. + if ( + cacheEntry && + cacheEntry.readTime < assetCacheRegistryStartTime + ) { + assetCacheRemove(assetKey); + } + } + if ( assetKeyToUpdate === undefined ) { + return updateDone(); + } + updaterFetched.add(assetKeyToUpdate); + + const result = await getRemote(assetKeyToUpdate); + if ( result.content !== '' ) { + updaterUpdated.push(result.assetKey); + if ( result.assetKey === 'assets.json' ) { + updateAssetSourceRegistry(result.content); + } + } else { + fireNotification('asset-update-failed', { assetKey: result.assetKey }); + } + + vAPI.setTimeout(updateNext, updaterAssetDelay); }; const updateDone = function() { diff --git a/src/js/messaging.js b/src/js/messaging.js index 347fe6ce6..353803597 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -58,9 +58,10 @@ const onMessage = function(request, sender, callback) { // https://github.com/chrisaljoudi/uBlock/issues/417 µb.assets.get( request.url, - { dontCache: true, needSourceURL: true }, - callback - ); + { dontCache: true, needSourceURL: true } + ).then(result => { + callback(result); + }); return; case 'listsFromNetFilter': @@ -791,24 +792,18 @@ vAPI.messaging.listen({ const µb = µBlock; // Settings -const getLocalData = function(callback) { - const onStorageInfoReady = function(bytesInUse) { - const o = µb.restoreBackupSettings; - callback({ - storageUsed: bytesInUse, - lastRestoreFile: o.lastRestoreFile, - lastRestoreTime: o.lastRestoreTime, - lastBackupFile: o.lastBackupFile, - lastBackupTime: o.lastBackupTime, - cloudStorageSupported: µb.cloudStorageSupported, - privacySettingsSupported: µb.privacySettingsSupported - }); - }; - - µb.getBytesInUse(onStorageInfoReady); +const getLocalData = async function() { + const data = Object.assign({}, µb.restoreBackupSettings); + data.storageUsed = await µb.getBytesInUse(); + return data; }; -const backupUserData = function(callback) { +const backupUserData = async function() { + const [ userFilters, localData ] = await Promise.all([ + µb.loadUserFilters(), + getLocalData(), + ]); + const userData = { timeStamp: Date.now(), version: vAPI.app.version, @@ -821,23 +816,17 @@ const backupUserData = function(callback) { dynamicFilteringString: µb.permanentFirewall.toString(), urlFilteringString: µb.permanentURLFiltering.toString(), hostnameSwitchesString: µb.permanentSwitches.toString(), - userFilters: '' + userFilters: userFilters.content, }; - const onUserFiltersReady = function(details) { - userData.userFilters = details.content; - const filename = vAPI.i18n('aboutBackupFilename') - .replace('{{datetime}}', µb.dateNowToSensibleString()) - .replace(/ +/g, '_'); - µb.restoreBackupSettings.lastBackupFile = filename; - µb.restoreBackupSettings.lastBackupTime = Date.now(); - vAPI.storage.set(µb.restoreBackupSettings); - getLocalData(function(localData) { - callback({ localData: localData, userData: userData }); - }); - }; + const filename = vAPI.i18n('aboutBackupFilename') + .replace('{{datetime}}', µb.dateNowToSensibleString()) + .replace(/ +/g, '_'); + µb.restoreBackupSettings.lastBackupFile = filename; + µb.restoreBackupSettings.lastBackupTime = Date.now(); + vAPI.storage.set(µb.restoreBackupSettings); - µb.assets.get(µb.userFiltersPath, onUserFiltersReady); + return { localData, userData }; }; const restoreUserData = function(request) { @@ -881,7 +870,7 @@ const restoreUserData = function(request) { lastBackupFile: '', lastBackupTime: 0 }); - µb.assets.put(µb.userFiltersPath, userData.userFilters); + µb.saveUserFilters(userData.userFilters); if ( Array.isArray(userData.selectedFilterLists) ) { µb.saveSelectedFilterLists(userData.selectedFilterLists, restart); } else { @@ -902,18 +891,17 @@ const restoreUserData = function(request) { // Remove all stored data but keep global counts, people can become // quite attached to numbers -const resetUserData = function() { - let count = 3; - const countdown = ( ) => { - count -= 1; - if ( count === 0 ) { - vAPI.app.restart(); - } - }; - µb.cacheStorage.clear().then(( ) => countdown()); // 1 - vAPI.storage.clear(countdown); // 2 - µb.saveLocalSettings(countdown); // 3 +const resetUserData = async function() { vAPI.localStorage.removeItem('immediateHiddenSettings'); + + await Promise.all([ + µb.cacheStorage.clear(), + vAPI.storage.clear(), + ]); + + await µb.saveLocalSettings(); + + vAPI.app.restart(); }; // 3rd-party filters @@ -932,7 +920,7 @@ const prepListEntries = function(entries) { } }; -const getLists = function(callback) { +const getLists = async function(callback) { const r = { autoUpdate: µb.userSettings.autoUpdate, available: null, @@ -946,17 +934,15 @@ const getLists = function(callback) { parseCosmeticFilters: µb.userSettings.parseAllABPHideFilters, userFiltersPath: µb.userFiltersPath }; - const onMetadataReady = function(entries) { - r.cache = entries; - prepListEntries(r.cache); - callback(r); - }; - const onLists = function(lists) { - r.available = lists; - prepListEntries(r.available); - µb.assets.metadata(onMetadataReady); - }; - µb.getAvailableLists(onLists); + const [ lists, metadata ] = await Promise.all([ + µb.getAvailableLists(), + µb.assets.metadata(), + ]); + r.available = lists; + prepListEntries(r.available); + r.cache = metadata; + prepListEntries(r.cache); + callback(r); }; // My rules @@ -1060,29 +1046,37 @@ const onMessage = function(request, sender, callback) { // Async switch ( request.what ) { case 'backupUserData': - return backupUserData(callback); + return backupUserData().then(data => { + callback(data); + }); case 'getLists': return getLists(callback); case 'getLocalData': - return getLocalData(callback); + return getLocalData().then(localData => { + callback(localData); + }); case 'getShortcuts': return getShortcuts(callback); case 'readUserFilters': - return µb.loadUserFilters(callback); + return µb.loadUserFilters().then(result => { + callback(result); + }); case 'writeUserFilters': - return µb.saveUserFilters(request.content, callback); + return µb.saveUserFilters(request.content).then(result => { + callback(result); + }); default: break; } // Sync - var response; + let response; switch ( request.what ) { case 'canUpdateShortcuts': diff --git a/src/js/redirect-engine.js b/src/js/redirect-engine.js index fc40b3880..c5720f346 100644 --- a/src/js/redirect-engine.js +++ b/src/js/redirect-engine.js @@ -573,21 +573,20 @@ RedirectEngine.prototype.toSelfie = function(path) { /******************************************************************************/ -RedirectEngine.prototype.fromSelfie = function(path) { - return µBlock.assets.get(`${path}/main`).then(details => { - let selfie; - try { - selfie = JSON.parse(details.content); - } catch (ex) { - } - if ( selfie instanceof Object === false ) { return false; } - this.rules = new Map(selfie.rules); - this.ruleSources = new Set(selfie.ruleSources); - this.ruleDestinations = new Set(selfie.ruleDestinations); - this.resetCache(); - this.modifyTime = Date.now(); - return true; - }); +RedirectEngine.prototype.fromSelfie = async function(path) { + const result = await µBlock.assets.get(`${path}/main`); + let selfie; + try { + selfie = JSON.parse(result.content); + } catch (ex) { + } + if ( selfie instanceof Object === false ) { return false; } + this.rules = new Map(selfie.rules); + this.ruleSources = new Set(selfie.ruleSources); + this.ruleDestinations = new Set(selfie.ruleDestinations); + this.resetCache(); + this.modifyTime = Date.now(); + return true; }; /******************************************************************************/ @@ -788,29 +787,26 @@ RedirectEngine.prototype.selfieFromResources = function() { ); }; -RedirectEngine.prototype.resourcesFromSelfie = function() { - return µBlock.assets.get( - 'compiled/redirectEngine/resources' - ).then(details => { - let selfie; - try { - selfie = JSON.parse(details.content); - } catch(ex) { - } - if ( - selfie instanceof Object === false || - selfie.version !== resourcesSelfieVersion || - Array.isArray(selfie.resources) === false - ) { - return false; - } - this.aliases = new Map(selfie.aliases); - this.resources = new Map(); - for ( const [ token, entry ] of selfie.resources ) { - this.resources.set(token, RedirectEntry.fromSelfie(entry)); - } - return true; - }); +RedirectEngine.prototype.resourcesFromSelfie = async function() { + const result = await µBlock.assets.get('compiled/redirectEngine/resources'); + let selfie; + try { + selfie = JSON.parse(result.content); + } catch(ex) { + } + if ( + selfie instanceof Object === false || + selfie.version !== resourcesSelfieVersion || + Array.isArray(selfie.resources) === false + ) { + return false; + } + this.aliases = new Map(selfie.aliases); + this.resources = new Map(); + for ( const [ token, entry ] of selfie.resources ) { + this.resources.set(token, RedirectEntry.fromSelfie(entry)); + } + return true; }; RedirectEngine.prototype.invalidateResourcesSelfie = function() { diff --git a/src/js/reverselookup.js b/src/js/reverselookup.js index 922c7e84a..94947650d 100644 --- a/src/js/reverselookup.js +++ b/src/js/reverselookup.js @@ -23,59 +23,64 @@ /******************************************************************************/ -µBlock.staticFilteringReverseLookup = (function() { +µBlock.staticFilteringReverseLookup = (( ) => { /******************************************************************************/ -var worker = null; -var workerTTL = 5 * 60 * 1000; -var workerTTLTimer = null; -var needLists = true; -var messageId = 1; -var pendingResponses = Object.create(null); +const workerTTL = 5 * 60 * 1000; +const pendingResponses = new Map(); + +let worker = null; +let workerTTLTimer; +let needLists = true; +let messageId = 1; /******************************************************************************/ -var onWorkerMessage = function(e) { - var msg = e.data; - var callback = pendingResponses[msg.id]; - delete pendingResponses[msg.id]; +const onWorkerMessage = function(e) { + const msg = e.data; + const callback = pendingResponses.get(msg.id); + pendingResponses.delete(msg.id); callback(msg.response); }; /******************************************************************************/ -var stopWorker = function() { - workerTTLTimer = null; - if ( worker === null ) { - return; +const stopWorker = function() { + if ( workerTTLTimer !== undefined ) { + clearTimeout(workerTTLTimer); + workerTTLTimer = undefined; } + if ( worker === null ) { return; } worker.terminate(); worker = null; needLists = true; - pendingResponses = Object.create(null); + pendingResponses.clear(); }; /******************************************************************************/ -var initWorker = function(callback) { +const initWorker = function() { if ( worker === null ) { worker = new Worker('js/reverselookup-worker.js'); worker.onmessage = onWorkerMessage; } - if ( needLists === false ) { - callback(); - return; + // The worker will be shutdown after n minutes without being used. + if ( workerTTLTimer !== undefined ) { + clearTimeout(workerTTLTimer); } + workerTTLTimer = vAPI.setTimeout(stopWorker, workerTTL); + if ( needLists === false ) { + return Promise.resolve(); + } needLists = false; - var entries = Object.create(null); - var countdown = 0; + const entries = new Map(); - var onListLoaded = function(details) { - var entry = entries[details.assetKey]; + const onListLoaded = function(details) { + const entry = entries.get(details.assetKey); // https://github.com/gorhill/uBlock/issues/536 // Use assetKey when there is no filter list title. @@ -89,44 +94,40 @@ var initWorker = function(callback) { content: details.content } }); - - countdown -= 1; - if ( countdown === 0 ) { - callback(); - } }; - var µb = µBlock; - var listKey, entry; - - for ( listKey in µb.availableFilterLists ) { + const µb = µBlock; + for ( const listKey in µb.availableFilterLists ) { if ( µb.availableFilterLists.hasOwnProperty(listKey) === false ) { continue; } - entry = µb.availableFilterLists[listKey]; + const entry = µb.availableFilterLists[listKey]; if ( entry.off === true ) { continue; } - entries[listKey] = { + entries.set(listKey, { title: listKey !== µb.userFiltersPath ? entry.title : vAPI.i18n('1pPageName'), supportURL: entry.supportURL || '' - }; - countdown += 1; + }); + } + if ( entries.size === 0 ) { + return Promise.resolve(); } - if ( countdown === 0 ) { - callback(); - return; - } - - for ( listKey in entries ) { - µb.getCompiledFilterList(listKey, onListLoaded); + const promises = []; + for ( const listKey of entries.keys() ) { + promises.push( + µb.getCompiledFilterList(listKey).then(details => { + onListLoaded(details); + }) + ); } + return Promise.all(promises); }; /******************************************************************************/ -var fromNetFilter = function(compiledFilter, rawFilter, callback) { +const fromNetFilter = async function(compiledFilter, rawFilter, callback) { if ( typeof callback !== 'function' ) { return; } @@ -136,32 +137,22 @@ var fromNetFilter = function(compiledFilter, rawFilter, callback) { return; } - if ( workerTTLTimer !== null ) { - clearTimeout(workerTTLTimer); - workerTTLTimer = null; - } + await initWorker(); - var onWorkerReady = function() { - var id = messageId++; - var message = { - what: 'fromNetFilter', - id: id, - compiledFilter: compiledFilter, - rawFilter: rawFilter - }; - pendingResponses[id] = callback; - worker.postMessage(message); - - // The worker will be shutdown after n minutes without being used. - workerTTLTimer = vAPI.setTimeout(stopWorker, workerTTL); + const id = messageId++; + const message = { + what: 'fromNetFilter', + id: id, + compiledFilter: compiledFilter, + rawFilter: rawFilter }; - - initWorker(onWorkerReady); + pendingResponses.set(id, callback); + worker.postMessage(message); }; /******************************************************************************/ -var fromCosmeticFilter = function(details, callback) { +const fromCosmeticFilter = async function(details, callback) { if ( typeof callback !== 'function' ) { return; } if ( details.rawFilter === '' ) { @@ -169,50 +160,38 @@ var fromCosmeticFilter = function(details, callback) { return; } - if ( workerTTLTimer !== null ) { - clearTimeout(workerTTLTimer); - workerTTLTimer = null; - } + await initWorker(); - let onWorkerReady = function() { - let id = messageId++; - let hostname = µBlock.URI.hostnameFromURI(details.url); - pendingResponses[id] = callback; - worker.postMessage({ - what: 'fromCosmeticFilter', - id: id, - domain: µBlock.URI.domainFromHostname(hostname), - hostname: hostname, - ignoreGeneric: µBlock.staticNetFilteringEngine - .matchStringGenericHide(details.url) === 2, - rawFilter: details.rawFilter - }); - - // The worker will be shutdown after n minutes without being used. - workerTTLTimer = vAPI.setTimeout(stopWorker, workerTTL); - }; - - initWorker(onWorkerReady); + const id = messageId++; + const hostname = µBlock.URI.hostnameFromURI(details.url); + pendingResponses.set(id, callback); + worker.postMessage({ + what: 'fromCosmeticFilter', + id: id, + domain: µBlock.URI.domainFromHostname(hostname), + hostname: hostname, + ignoreGeneric: µBlock.staticNetFilteringEngine + .matchStringGenericHide(details.url) === 2, + rawFilter: details.rawFilter + }); }; /******************************************************************************/ // This tells the worker that filter lists may have changed. -var resetLists = function() { +const resetLists = function() { needLists = true; - if ( worker === null ) { - return; - } + if ( worker === null ) { return; } worker.postMessage({ what: 'resetLists' }); }; /******************************************************************************/ return { - fromNetFilter: fromNetFilter, - fromCosmeticFilter: fromCosmeticFilter, - resetLists: resetLists, + fromNetFilter, + fromCosmeticFilter, + resetLists, shutdown: stopWorker }; diff --git a/src/js/start.js b/src/js/start.js index 561da8959..45d42b5d4 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -25,8 +25,8 @@ // Load all: executed once. -{ -// >>>>> start of local scope +(async ( ) => { +// >>>>> start of private scope const µb = µBlock; @@ -97,8 +97,6 @@ const onAllReady = function() { vAPI.app.restart(); } }); - - log.info(`All ready ${Date.now()-vAPI.T0} ms after launch`); }; /******************************************************************************/ @@ -152,27 +150,6 @@ const initializeTabs = function() { /******************************************************************************/ -// Filtering engines dependencies: -// - PSL - -const onPSLReady = function() { - log.info(`PSL ready ${Date.now()-vAPI.T0} ms after launch`); - - µb.selfieManager.load().then(valid => { - if ( valid === true ) { - log.info(`Selfie ready ${Date.now()-vAPI.T0} ms after launch`); - onAllReady(); - return; - } - µb.loadFilterLists(( ) => { - log.info(`Filter lists ready ${Date.now()-vAPI.T0} ms after launch`); - onAllReady(); - }); - }); -}; - -/******************************************************************************/ - const onCommandShortcutsReady = function(commandShortcuts) { if ( Array.isArray(commandShortcuts) === false ) { return; } µb.commandShortcuts = new Map(commandShortcuts); @@ -226,8 +203,6 @@ const onNetWhitelistReady = function(netWhitelistRaw) { // User settings are in memory const onUserSettingsReady = function(fetched) { - log.info(`User settings ready ${Date.now()-vAPI.T0} ms after launch`); - const userSettings = µb.userSettings; fromFetch(userSettings, fetched); @@ -271,8 +246,6 @@ const onSystemSettingsReady = function(fetched) { /******************************************************************************/ const onFirstFetchReady = function(fetched) { - log.info(`First fetch ready ${Date.now()-vAPI.T0} ms after launch`); - // https://github.com/uBlockOrigin/uBlock-issues/issues/507 // Firefox-specific: somehow `fetched` is undefined under certain // circumstances even though we asked to load with default values. @@ -291,10 +264,6 @@ const onFirstFetchReady = function(fetched) { onNetWhitelistReady(fetched.netWhitelist); onVersionReady(fetched.version); onCommandShortcutsReady(fetched.commandShortcuts); - - µb.loadPublicSuffixList().then(( ) => { - onPSLReady(); - }); }; /******************************************************************************/ @@ -347,42 +316,45 @@ const createDefaultProps = function() { /******************************************************************************/ -const onHiddenSettingsReady = function() { - return µb.cacheStorage.select( - µb.hiddenSettings.cacheStorageAPI - ).then(backend => { - log.info(`Backend storage for cache will be ${backend}`); - }); -}; - -/******************************************************************************/ - -// TODO(seamless migration): -// Eventually selected filter list keys will be loaded as a fetchable -// property. Until then we need to handle backward and forward -// compatibility, this means a special asynchronous call to load selected -// filter lists. - -const onAdminSettingsRestored = function() { +try { + // https://github.com/gorhill/uBlock/issues/531 + await µb.restoreAdminSettings(); log.info(`Admin settings ready ${Date.now()-vAPI.T0} ms after launch`); - Promise.all([ - µb.loadHiddenSettings().then(( ) => - onHiddenSettingsReady() - ), - µb.loadSelectedFilterLists(), - ]).then(( ) => { - log.info(`List selection ready ${Date.now()-vAPI.T0} ms after launch`); - vAPI.storage.get(createDefaultProps(), onFirstFetchReady); - }); -}; + await µb.loadHiddenSettings(); + log.info(`Hidden settings ready ${Date.now()-vAPI.T0} ms after launch`); -/******************************************************************************/ + const cacheBackend = await µb.cacheStorage.select( + µb.hiddenSettings.cacheStorageAPI + ); + log.info(`Backend storage for cache will be ${cacheBackend}`); -// https://github.com/gorhill/uBlock/issues/531 -µb.restoreAdminSettings().then(( ) => { - onAdminSettingsRestored(); -}); + await Promise.all([ + µb.loadSelectedFilterLists().then(( ) => { + log.info(`List selection ready ${Date.now()-vAPI.T0} ms after launch`); + }), + vAPI.storage.get(createDefaultProps()).then(fetched => { + log.info(`First fetch ready ${Date.now()-vAPI.T0} ms after launch`); + onFirstFetchReady(fetched); + }), + µb.loadPublicSuffixList().then(( ) => { + log.info(`PSL ready ${Date.now()-vAPI.T0} ms after launch`); + }), + ]); -// <<<<< end of local scope + const selfieIsValid = await µb.selfieManager.load(); + if ( selfieIsValid === true ) { + log.info(`Selfie ready ${Date.now()-vAPI.T0} ms after launch`); + } else { + await µb.loadFilterLists(); + log.info(`Filter lists ready ${Date.now()-vAPI.T0} ms after launch`); + } +} catch (ex) { + console.trace(ex); } + +onAllReady(); +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 f9af48ebc..7e80f1c3e 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -25,48 +25,44 @@ /******************************************************************************/ -µBlock.getBytesInUse = function(callback) { - if ( typeof callback !== 'function' ) { - callback = this.noopFunc; - } +µBlock.getBytesInUse = async function() { + const promises = []; let bytesInUse; - let countdown = 0; - - const process = count => { - if ( typeof count === 'number' ) { - if ( bytesInUse === undefined ) { - bytesInUse = 0; - } - bytesInUse += count; - } - countdown -= 1; - if ( countdown > 0 ) { return; } - µBlock.storageUsed = bytesInUse; - callback(bytesInUse); - }; // Not all platforms implement this method. - if ( vAPI.storage.getBytesInUse instanceof Function ) { - countdown += 1; - vAPI.storage.getBytesInUse(null, process); - } + promises.push( + vAPI.storage.getBytesInUse instanceof Function + ? vAPI.storage.getBytesInUse(null) + : undefined + ); + if ( navigator.storage instanceof Object && navigator.storage.estimate instanceof Function ) { - countdown += 1; - navigator.storage.estimate().then(estimate => { - process(estimate.usage); - }); + promises.push(navigator.storage.estimate()); } - if ( countdown === 0 ) { - callback(); + + const results = await Promise.all(promises); + + const processCount = count => { + if ( typeof count !== 'number' ) { return; } + if ( bytesInUse === undefined ) { bytesInUse = 0; } + bytesInUse += count; + return bytesInUse; + }; + + processCount(results[0]); + if ( results.length > 1 && results[1] instanceof Object ) { + processCount(results[1].usage); } + µBlock.storageUsed = bytesInUse; + return bytesInUse; }; /******************************************************************************/ -µBlock.saveLocalSettings = (function() { +µBlock.saveLocalSettings = (( ) => { const saveAfter = 4 * 60 * 1000; const onTimeout = ( ) => { @@ -79,9 +75,9 @@ vAPI.setTimeout(onTimeout, saveAfter); - return function(callback) { + return function() { this.localSettingsLastSaved = Date.now(); - vAPI.storage.set(this.localSettings, callback); + return vAPI.storage.set(this.localSettings); }; })(); @@ -93,40 +89,31 @@ /******************************************************************************/ -µBlock.loadHiddenSettings = function() { - return new Promise(resolve => { - // >>>> start of executor +µBlock.loadHiddenSettings = async function() { + const bin = await vAPI.storage.get('hiddenSettings'); + if ( bin instanceof Object === false ) { return; } - vAPI.storage.get('hiddenSettings', bin => { - if ( bin instanceof Object === false ) { - return resolve(); - } - const hs = bin.hiddenSettings; - if ( hs instanceof Object ) { - const hsDefault = this.hiddenSettingsDefault; - for ( const key in hsDefault ) { - if ( - hsDefault.hasOwnProperty(key) && - hs.hasOwnProperty(key) && - typeof hs[key] === typeof hsDefault[key] - ) { - this.hiddenSettings[key] = hs[key]; - } - } - if ( typeof this.hiddenSettings.suspendTabsUntilReady === 'boolean' ) { - this.hiddenSettings.suspendTabsUntilReady = - this.hiddenSettings.suspendTabsUntilReady - ? 'yes' - : 'unset'; + const hs = bin.hiddenSettings; + if ( hs instanceof Object ) { + const hsDefault = this.hiddenSettingsDefault; + for ( const key in hsDefault ) { + if ( + hsDefault.hasOwnProperty(key) && + hs.hasOwnProperty(key) && + typeof hs[key] === typeof hsDefault[key] + ) { + this.hiddenSettings[key] = hs[key]; } } - self.log.verbosity = this.hiddenSettings.consoleLogLevel; - resolve(); - this.fireDOMEvent('hiddenSettingsChanged'); - }); - - // <<<< end of executor - }); + if ( typeof this.hiddenSettings.suspendTabsUntilReady === 'boolean' ) { + this.hiddenSettings.suspendTabsUntilReady = + this.hiddenSettings.suspendTabsUntilReady + ? 'yes' + : 'unset'; + } + } + self.log.verbosity = this.hiddenSettings.consoleLogLevel; + this.fireDOMEvent('hiddenSettingsChanged'); }; // Note: Save only the settings which values differ from the default ones. @@ -261,30 +248,16 @@ **/ -µBlock.loadSelectedFilterLists = function() { - return new Promise(resolve => { - // >>>> start of executor - - vAPI.storage.get('selectedFilterLists', bin => { - // Select default filter lists if first-time launch. - if ( - bin instanceof Object === false || - Array.isArray(bin.selectedFilterLists) === false - ) { - this.assets.metadata(availableLists => { - this.saveSelectedFilterLists( - this.autoSelectRegionalFilterLists(availableLists) - ); - resolve(); - }); - return; - } +µBlock.loadSelectedFilterLists = async function() { + const bin = await vAPI.storage.get('selectedFilterLists'); + if ( bin instanceof Object && Array.isArray(bin.selectedFilterLists) ) { this.selectedFilterLists = bin.selectedFilterLists; - resolve(); - }); + return; + } - // <<<< end of executor - }); + // Select default filter lists if first-time launch. + const lists = await this.assets.metadata(); + this.saveSelectedFilterLists(this.autoSelectRegionalFilterLists(lists)); }; µBlock.saveSelectedFilterLists = function(newKeys, append, callback) { @@ -409,20 +382,20 @@ /******************************************************************************/ -µBlock.saveUserFilters = function(content, callback) { +µBlock.saveUserFilters = function(content) { // https://github.com/gorhill/uBlock/issues/1022 // Be sure to end with an empty line. content = content.trim(); if ( content !== '' ) { content += '\n'; } - this.assets.put(this.userFiltersPath, content, callback); this.removeCompiledFilterList(this.userFiltersPath); + return this.assets.put(this.userFiltersPath, content); }; -µBlock.loadUserFilters = function(callback) { - return this.assets.get(this.userFiltersPath, callback); +µBlock.loadUserFilters = function() { + return this.assets.get(this.userFiltersPath); }; -µBlock.appendUserFilters = function(filters, options) { +µBlock.appendUserFilters = async function(filters, options) { filters = filters.trim(); if ( filters.length === 0 ) { return; } @@ -443,59 +416,56 @@ .replace('{{origin}}', options.origin); } - const onSaved = ( ) => { - const compiledFilters = this.compileFilters( - filters, - { assetKey: this.userFiltersPath } - ); - const snfe = this.staticNetFilteringEngine; - const cfe = this.cosmeticFilteringEngine; - const acceptedCount = snfe.acceptedCount + cfe.acceptedCount; - const discardedCount = snfe.discardedCount + cfe.discardedCount; - this.applyCompiledFilters(compiledFilters, true); - const entry = this.availableFilterLists[this.userFiltersPath]; - const deltaEntryCount = - snfe.acceptedCount + - cfe.acceptedCount - acceptedCount; - const deltaEntryUsedCount = - deltaEntryCount - - (snfe.discardedCount + cfe.discardedCount - discardedCount); - entry.entryCount += deltaEntryCount; - entry.entryUsedCount += deltaEntryUsedCount; - vAPI.storage.set({ 'availableFilterLists': this.availableFilterLists }); - this.staticNetFilteringEngine.freeze(); - this.redirectEngine.freeze(); - this.staticExtFilteringEngine.freeze(); - this.selfieManager.destroy(); + const details = await this.loadUserFilters(); + if ( details.error ) { return; } - // https://www.reddit.com/r/uBlockOrigin/comments/cj7g7m/ - // https://www.reddit.com/r/uBlockOrigin/comments/cnq0bi/ - if ( options.killCache ) { - browser.webRequest.handlerBehaviorChanged(); + // The comment, if any, will be applied if and only if it is different + // from the last comment found in the user filter list. + if ( comment !== '' ) { + const pos = details.content.lastIndexOf(comment); + if ( + pos === -1 || + details.content.indexOf('\n!', pos + 1) !== -1 + ) { + filters = '\n' + comment + '\n' + filters; } - }; + } - const onLoaded = details => { - if ( details.error ) { return; } - // The comment, if any, will be applied if and only if it is different - // from the last comment found in the user filter list. - if ( comment !== '' ) { - const pos = details.content.lastIndexOf(comment); - if ( - pos === -1 || - details.content.indexOf('\n!', pos + 1) !== -1 - ) { - filters = '\n' + comment + '\n' + filters; - } - } - // https://github.com/chrisaljoudi/uBlock/issues/976 - // If we reached this point, the filter quite probably needs to be - // added for sure: do not try to be too smart, trying to avoid - // duplicates at this point may lead to more issues. - this.saveUserFilters(details.content.trim() + '\n' + filters, onSaved); - }; + // https://github.com/chrisaljoudi/uBlock/issues/976 + // If we reached this point, the filter quite probably needs to be + // added for sure: do not try to be too smart, trying to avoid + // duplicates at this point may lead to more issues. + await this.saveUserFilters(details.content.trim() + '\n' + filters); - this.loadUserFilters(onLoaded); + const compiledFilters = this.compileFilters( + filters, + { assetKey: this.userFiltersPath } + ); + const snfe = this.staticNetFilteringEngine; + const cfe = this.cosmeticFilteringEngine; + const acceptedCount = snfe.acceptedCount + cfe.acceptedCount; + const discardedCount = snfe.discardedCount + cfe.discardedCount; + this.applyCompiledFilters(compiledFilters, true); + const entry = this.availableFilterLists[this.userFiltersPath]; + const deltaEntryCount = + snfe.acceptedCount + + cfe.acceptedCount - acceptedCount; + const deltaEntryUsedCount = + deltaEntryCount - + (snfe.discardedCount + cfe.discardedCount - discardedCount); + entry.entryCount += deltaEntryCount; + entry.entryUsedCount += deltaEntryUsedCount; + vAPI.storage.set({ 'availableFilterLists': this.availableFilterLists }); + this.staticNetFilteringEngine.freeze(); + this.redirectEngine.freeze(); + this.staticExtFilteringEngine.freeze(); + this.selfieManager.destroy(); + + // https://www.reddit.com/r/uBlockOrigin/comments/cj7g7m/ + // https://www.reddit.com/r/uBlockOrigin/comments/cnq0bi/ + if ( options.killCache ) { + browser.webRequest.handlerBehaviorChanged(); + } }; µBlock.createUserFilters = function(details) { @@ -525,7 +495,7 @@ /******************************************************************************/ -µBlock.getAvailableLists = function(callback) { +µBlock.getAvailableLists = async function() { let oldAvailableLists = {}, newAvailableLists = {}; @@ -577,105 +547,87 @@ this.saveSelectedFilterLists([ listURL ], true); }; + // Load previously saved available lists -- these contains data + // computed at run-time, we will reuse this data if possible. + const [ bin, entries ] = await Promise.all([ + vAPI.storage.get('availableFilterLists'), + this.assets.metadata(), + ]); + + oldAvailableLists = bin && bin.availableFilterLists || {}; + + for ( const assetKey in entries ) { + if ( entries.hasOwnProperty(assetKey) === false ) { continue; } + const entry = entries[assetKey]; + if ( entry.content !== 'filters' ) { continue; } + newAvailableLists[assetKey] = Object.assign({}, entry); + } + + // Load set of currently selected filter lists. + const listKeySet = new Set(this.selectedFilterLists); + for ( const listKey in newAvailableLists ) { + if ( newAvailableLists.hasOwnProperty(listKey) ) { + newAvailableLists[listKey].off = !listKeySet.has(listKey); + } + } + + //finalize(); // Final steps: // - reuse existing list metadata if any; // - unregister unreferenced imported filter lists if any. - const finalize = ( ) => { - // Reuse existing metadata. - for ( const assetKey in oldAvailableLists ) { - const oldEntry = oldAvailableLists[assetKey]; - const newEntry = newAvailableLists[assetKey]; - // List no longer exists. If a stock list, try to convert to - // imported list if it was selected. - if ( newEntry === undefined ) { - this.removeFilterList(assetKey); - if ( assetKey.indexOf('://') === -1 ) { - customListFromStockList(assetKey); - } - continue; - } - if ( oldEntry.entryCount !== undefined ) { - newEntry.entryCount = oldEntry.entryCount; - } - if ( oldEntry.entryUsedCount !== undefined ) { - newEntry.entryUsedCount = oldEntry.entryUsedCount; - } - // This may happen if the list name was pulled from the list - // content. - // https://github.com/chrisaljoudi/uBlock/issues/982 - // There is no guarantee the title was successfully extracted from - // the list content. - if ( - newEntry.title === '' && - typeof oldEntry.title === 'string' && - oldEntry.title !== '' - ) { - newEntry.title = oldEntry.title; - } - } - - // Remove unreferenced imported filter lists. - const dict = new Set(importedListKeys); - for ( const assetKey in newAvailableLists ) { - const newEntry = newAvailableLists[assetKey]; - if ( newEntry.submitter !== 'user' ) { continue; } - if ( dict.has(assetKey) ) { continue; } - delete newAvailableLists[assetKey]; - this.assets.unregisterAssetSource(assetKey); + // Reuse existing metadata. + for ( const assetKey in oldAvailableLists ) { + const oldEntry = oldAvailableLists[assetKey]; + const newEntry = newAvailableLists[assetKey]; + // List no longer exists. If a stock list, try to convert to + // imported list if it was selected. + if ( newEntry === undefined ) { this.removeFilterList(assetKey); - } - }; - - // Built-in filter lists loaded. - const onBuiltinListsLoaded = entries => { - for ( const assetKey in entries ) { - if ( entries.hasOwnProperty(assetKey) === false ) { continue; } - const entry = entries[assetKey]; - if ( entry.content !== 'filters' ) { continue; } - newAvailableLists[assetKey] = Object.assign({}, entry); - } - - // Load set of currently selected filter lists. - const listKeySet = new Set(this.selectedFilterLists); - for ( const listKey in newAvailableLists ) { - if ( newAvailableLists.hasOwnProperty(listKey) ) { - newAvailableLists[listKey].off = !listKeySet.has(listKey); + if ( assetKey.indexOf('://') === -1 ) { + customListFromStockList(assetKey); } + continue; } + if ( oldEntry.entryCount !== undefined ) { + newEntry.entryCount = oldEntry.entryCount; + } + if ( oldEntry.entryUsedCount !== undefined ) { + newEntry.entryUsedCount = oldEntry.entryUsedCount; + } + // This may happen if the list name was pulled from the list + // content. + // https://github.com/chrisaljoudi/uBlock/issues/982 + // There is no guarantee the title was successfully extracted from + // the list content. + if ( + newEntry.title === '' && + typeof oldEntry.title === 'string' && + oldEntry.title !== '' + ) { + newEntry.title = oldEntry.title; + } + } - finalize(); - callback(newAvailableLists); - }; + // Remove unreferenced imported filter lists. + for ( const assetKey in newAvailableLists ) { + const newEntry = newAvailableLists[assetKey]; + if ( newEntry.submitter !== 'user' ) { continue; } + if ( importedListKeys.indexOf(assetKey) !== -1 ) { continue; } + delete newAvailableLists[assetKey]; + this.assets.unregisterAssetSource(assetKey); + this.removeFilterList(assetKey); + } - // Available lists previously computed. - const onOldAvailableListsLoaded = bin => { - oldAvailableLists = bin && bin.availableFilterLists || {}; - this.assets.metadata(onBuiltinListsLoaded); - }; - - // Load previously saved available lists -- these contains data - // computed at run-time, we will reuse this data if possible. - vAPI.storage.get('availableFilterLists', onOldAvailableListsLoaded); + return newAvailableLists; }; /******************************************************************************/ -// This is used to be re-entrancy resistant. -µBlock.loadingFilterLists = false; - -µBlock.loadFilterLists = function(callback) { - // Callers are expected to check this first. - if ( this.loadingFilterLists ) { return; } - this.loadingFilterLists = true; - +µBlock.loadFilterLists = (( ) => { const loadedListKeys = []; - let filterlistsCount = 0; + let loadingPromise; - if ( typeof callback !== 'function' ) { - callback = this.noopFunc; - } - - const onDone = ( ) => { + const onDone = function() { this.staticNetFilteringEngine.freeze(); this.staticExtFilteringEngine.freeze(); this.redirectEngine.freeze(); @@ -690,15 +642,13 @@ listKeys: loadedListKeys }); - callback(); - this.selfieManager.destroy(); this.lz4Codec.relinquish(); - this.loadingFilterLists = false; + loadingPromise = undefined; }; - const applyCompiledFilters = (assetKey, compiled) => { + const applyCompiledFilters = function(assetKey, compiled) { const snfe = this.staticNetFilteringEngine; const sxfe = this.staticExtFilteringEngine; let acceptedCount = snfe.acceptedCount + sxfe.acceptedCount, @@ -714,15 +664,7 @@ loadedListKeys.push(assetKey); }; - const onCompiledListLoaded = details => { - applyCompiledFilters(details.assetKey, details.content); - filterlistsCount -= 1; - if ( filterlistsCount === 0 ) { - onDone(); - } - }; - - const onFilterListsReady = lists => { + const onFilterListsReady = function(lists) { this.availableFilterLists = lists; vAPI.net.suspend(); @@ -740,66 +682,71 @@ for ( const assetKey in lists ) { if ( lists.hasOwnProperty(assetKey) === false ) { continue; } if ( lists[assetKey].off ) { continue; } - toLoad.push(assetKey); - } - filterlistsCount = toLoad.length; - if ( filterlistsCount === 0 ) { - return onDone(); + + toLoad.push( + this.getCompiledFilterList(assetKey).then(details => { + applyCompiledFilters.call( + this, + details.assetKey, + details.content + ); + }) + ); } - let i = toLoad.length; - while ( i-- ) { - this.getCompiledFilterList(toLoad[i], onCompiledListLoaded); - } + return Promise.all(toLoad); }; - this.getAvailableLists(onFilterListsReady); - this.loadRedirectResources(); -}; + return function() { + if ( loadingPromise instanceof Promise === false ) { + loadedListKeys.length = 0; + loadingPromise = Promise.all([ + this.getAvailableLists().then(lists => { + return onFilterListsReady.call(this, lists); + }), + this.loadRedirectResources(), + ]).then(( ) => { + onDone.call(this); + }); + } + return loadingPromise; + }; +})(); /******************************************************************************/ -µBlock.getCompiledFilterList = function(assetKey, callback) { +µBlock.getCompiledFilterList = async function(assetKey) { const compiledPath = 'compiled/' + assetKey; - let rawContent; - const onCompiledListLoaded2 = details => { - if ( details.content === '' ) { - details.content = this.compileFilters( - rawContent, - { assetKey: assetKey } - ); - this.assets.put(compiledPath, details.content); - } - rawContent = undefined; - details.assetKey = assetKey; - callback(details); - }; + let compiledDetails = await this.assets.get(compiledPath); + if ( compiledDetails.content !== '' ) { + compiledDetails.assetKey = assetKey; + return compiledDetails; + } - const onRawListLoaded = details => { - if ( details.content === '' ) { - details.assetKey = assetKey; - callback(details); - return; - } - this.extractFilterListMetadata(assetKey, details.content); - // Fectching the raw content may cause the compiled content to be - // generated somewhere else in uBO, hence we try one last time to - // fetch the compiled content in case it has become available. - rawContent = details.content; - this.assets.get(compiledPath, onCompiledListLoaded2); - }; + const rawDetails = await this.assets.get(assetKey); + // Compiling an empty string results in an empty string. + if ( rawDetails.content === '' ) { + rawDetails.assetKey = assetKey; + return rawDetails; + } - const onCompiledListLoaded1 = details => { - if ( details.content === '' ) { - this.assets.get(assetKey, onRawListLoaded); - return; - } - details.assetKey = assetKey; - callback(details); - }; + this.extractFilterListMetadata(assetKey, rawDetails.content); - this.assets.get(compiledPath, onCompiledListLoaded1); + // Fetching the raw content may cause the compiled content to be + // generated somewhere else in uBO, hence we try one last time to + // fetch the compiled content in case it has become available. + compiledDetails = await this.assets.get(compiledPath); + if ( compiledDetails.content === '' ) { + compiledDetails.content = this.compileFilters( + rawDetails.content, + { assetKey: assetKey } + ); + this.assets.put(compiledPath, compiledDetails.content); + } + + compiledDetails.assetKey = assetKey; + return compiledDetails; }; /******************************************************************************/ @@ -851,7 +798,7 @@ /******************************************************************************/ µBlock.compileFilters = function(rawText, details) { - let writer = new this.CompiledLineIO.Writer(); + const writer = new this.CompiledLineIO.Writer(); // Populate the writer with information potentially useful to the // client compilers. @@ -1007,8 +954,9 @@ /******************************************************************************/ -µBlock.loadRedirectResources = function() { - return this.redirectEngine.resourcesFromSelfie().then(success => { +µBlock.loadRedirectResources = async function() { + try { + const success = await this.redirectEngine.resourcesFromSelfie(); if ( success === true ) { return true; } const fetchPromises = [ @@ -1022,8 +970,7 @@ } } - return Promise.all(fetchPromises); - }).then(results => { + const results = await Promise.all(fetchPromises); if ( Array.isArray(results) === false ) { return results; } let content = ''; @@ -1041,35 +988,34 @@ this.redirectEngine.resourcesFromString(content); this.redirectEngine.selfieFromResources(); - return true; - }).catch(reason => { - log.info(reason); + } catch(ex) { + log.info(ex); return false; - }); + } + return true; }; /******************************************************************************/ -µBlock.loadPublicSuffixList = function() { +µBlock.loadPublicSuffixList = async function() { if ( this.hiddenSettings.disableWebAssembly === false ) { publicSuffixList.enableWASM(); } - return this.assets.get( - 'compiled/' + this.pslAssetKey - ).then(details => - publicSuffixList.fromSelfie(details.content, µBlock.base64) - ).catch(reason => { - console.info(reason); - return false; - }).then(success => { - if ( success ) { return; } - return this.assets.get(this.pslAssetKey, details => { - if ( details.content !== '' ) { - this.compilePublicSuffixList(details.content); - } - }); - }); + try { + const result = await this.assets.get(`compiled/${this.pslAssetKey}`); + if ( publicSuffixList.fromSelfie(result.content, this.base64) ) { + return; + } + } catch (ex) { + console.error(ex); + return; + } + + const result = await this.assets.get(this.pslAssetKey); + if ( result.content !== '' ) { + this.compilePublicSuffixList(result.content); + } }; µBlock.compilePublicSuffixList = function(content) { @@ -1111,7 +1057,7 @@ }); }; - const load = function() { + const load = async function() { return Promise.all([ µb.assets.get('selfie/main').then(details => { if ( @@ -1162,10 +1108,7 @@ }, µb.hiddenSettings.selfieAfter * 60000); }; - return { - load: load, - destroy: destroy - }; + return { load, destroy }; })(); /******************************************************************************/ @@ -1177,97 +1120,82 @@ // necessarily present, i.e. administrators may removed entries which // values are left to the user's choice. -µBlock.restoreAdminSettings = function() { - return new Promise(resolve => { - // >>>> start of executor - - if ( vAPI.adminStorage instanceof Object === false ) { - return resolve(); +µBlock.restoreAdminSettings = async function() { + let data; + try { + const json = await vAPI.adminStorage.getItem('adminSettings'); + if ( typeof json === 'string' && json !== '' ) { + data = JSON.parse(json); + } + } catch (ex) { + console.error(ex); } - vAPI.adminStorage.getItem('adminSettings', json => { - let data; - if ( typeof json === 'string' && json !== '' ) { - try { - data = JSON.parse(json); - } catch (ex) { - console.error(ex); + if ( data instanceof Object === false ) { return; } + + const bin = {}; + let binNotEmpty = false; + + // https://github.com/uBlockOrigin/uBlock-issues/issues/666 + // Allows an admin to set their own 'assets.json' file, with their + // own set of stock assets. + if ( + typeof data.assetsBootstrapLocation === 'string' && + data.assetsBootstrapLocation !== '' + ) { + µBlock.assetsBootstrapLocation = data.assetsBootstrapLocation; + } + + if ( typeof data.userSettings === 'object' ) { + for ( const name in this.userSettings ) { + if ( this.userSettings.hasOwnProperty(name) === false ) { + continue; } - } - - if ( data instanceof Object === false ) { - return resolve(); - } - - const bin = {}; - let binNotEmpty = false; - - // https://github.com/uBlockOrigin/uBlock-issues/issues/666 - // Allows an admin to set their own 'assets.json' file, with their - // own set of stock assets. - if ( - typeof data.assetsBootstrapLocation === 'string' && - data.assetsBootstrapLocation !== '' - ) { - µBlock.assetsBootstrapLocation = data.assetsBootstrapLocation; - } - - if ( typeof data.userSettings === 'object' ) { - for ( const name in this.userSettings ) { - if ( this.userSettings.hasOwnProperty(name) === false ) { - continue; - } - if ( data.userSettings.hasOwnProperty(name) === false ) { - continue; - } - bin[name] = data.userSettings[name]; - binNotEmpty = true; + if ( data.userSettings.hasOwnProperty(name) === false ) { + continue; } - } - - // 'selectedFilterLists' is an array of filter list tokens. Each token - // is a reference to an asset in 'assets.json'. - if ( Array.isArray(data.selectedFilterLists) ) { - bin.selectedFilterLists = data.selectedFilterLists; + bin[name] = data.userSettings[name]; binNotEmpty = true; } + } - if ( Array.isArray(data.whitelist) ) { - bin.netWhitelist = data.whitelist; - binNotEmpty = true; - } else if ( typeof data.netWhitelist === 'string' ) { - bin.netWhitelist = data.netWhitelist.split('\n'); - binNotEmpty = true; - } + // 'selectedFilterLists' is an array of filter list tokens. Each token + // is a reference to an asset in 'assets.json'. + if ( Array.isArray(data.selectedFilterLists) ) { + bin.selectedFilterLists = data.selectedFilterLists; + binNotEmpty = true; + } - if ( typeof data.dynamicFilteringString === 'string' ) { - bin.dynamicFilteringString = data.dynamicFilteringString; - binNotEmpty = true; - } + if ( Array.isArray(data.whitelist) ) { + bin.netWhitelist = data.whitelist; + binNotEmpty = true; + } else if ( typeof data.netWhitelist === 'string' ) { + bin.netWhitelist = data.netWhitelist.split('\n'); + binNotEmpty = true; + } - if ( typeof data.urlFilteringString === 'string' ) { - bin.urlFilteringString = data.urlFilteringString; - binNotEmpty = true; - } + if ( typeof data.dynamicFilteringString === 'string' ) { + bin.dynamicFilteringString = data.dynamicFilteringString; + binNotEmpty = true; + } - if ( typeof data.hostnameSwitchesString === 'string' ) { - bin.hostnameSwitchesString = data.hostnameSwitchesString; - binNotEmpty = true; - } + if ( typeof data.urlFilteringString === 'string' ) { + bin.urlFilteringString = data.urlFilteringString; + binNotEmpty = true; + } - if ( binNotEmpty ) { - vAPI.storage.set(bin); - } + if ( typeof data.hostnameSwitchesString === 'string' ) { + bin.hostnameSwitchesString = data.hostnameSwitchesString; + binNotEmpty = true; + } - if ( typeof data.userFilters === 'string' ) { - this.assets.put(this.userFiltersPath, data.userFilters); - } + if ( binNotEmpty ) { + vAPI.storage.set(bin); + } - resolve(); - }); - - // <<<< end of executor - }); + if ( typeof data.userFilters === 'string' ) { + this.saveUserFilters(data.userFilters); + } }; /******************************************************************************/