mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-17 16:02:33 +01:00
de0f6a2cdd
Related issue: - https://github.com/uBlockOrigin/uBlock-issues/issues/1865 This may be useful to rule out that the issue is caused by the browser API responding in unduly delayed manner.
1063 lines
34 KiB
JavaScript
1063 lines
34 KiB
JavaScript
/*******************************************************************************
|
|
|
|
uBlock Origin - a browser extension to block requests.
|
|
Copyright (C) 2014-present Raymond Hill
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
|
|
|
Home: https://github.com/gorhill/uBlock
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/******************************************************************************/
|
|
|
|
import cacheStorage from './cachestorage.js';
|
|
import logger from './logger.js';
|
|
import µb from './background.js';
|
|
|
|
/******************************************************************************/
|
|
|
|
const reIsExternalPath = /^(?:[a-z-]+):\/\//;
|
|
const reIsUserAsset = /^user-/;
|
|
const errorCantConnectTo = vAPI.i18n('errorCantConnectTo');
|
|
|
|
const assets = {};
|
|
|
|
// A hint for various pieces of code to take measures if possible to save
|
|
// bandwidth of remote servers.
|
|
let remoteServerFriendly = false;
|
|
|
|
/******************************************************************************/
|
|
|
|
const observers = [];
|
|
|
|
assets.addObserver = function(observer) {
|
|
if ( observers.indexOf(observer) === -1 ) {
|
|
observers.push(observer);
|
|
}
|
|
};
|
|
|
|
assets.removeObserver = function(observer) {
|
|
let pos;
|
|
while ( (pos = observers.indexOf(observer)) !== -1 ) {
|
|
observers.splice(pos, 1);
|
|
}
|
|
};
|
|
|
|
const fireNotification = function(topic, details) {
|
|
let result;
|
|
for ( const observer of observers ) {
|
|
const r = observer(topic, details);
|
|
if ( r !== undefined ) { result = r; }
|
|
}
|
|
return result;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
assets.fetch = function(url, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
// Start of executor
|
|
|
|
const timeoutAfter = µb.hiddenSettings.assetFetchTimeout * 1000 || 30000;
|
|
const xhr = new XMLHttpRequest();
|
|
let contentLoaded = 0;
|
|
let timeoutTimer;
|
|
|
|
const cleanup = function() {
|
|
xhr.removeEventListener('load', onLoadEvent);
|
|
xhr.removeEventListener('error', onErrorEvent);
|
|
xhr.removeEventListener('abort', onErrorEvent);
|
|
xhr.removeEventListener('progress', onProgressEvent);
|
|
if ( timeoutTimer !== undefined ) {
|
|
clearTimeout(timeoutTimer);
|
|
timeoutTimer = undefined;
|
|
}
|
|
};
|
|
|
|
const fail = function(details, msg) {
|
|
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,
|
|
statusCode: this.status || 200,
|
|
statusText: this.statusText || ''
|
|
};
|
|
if ( details.statusCode < 200 || details.statusCode >= 300 ) {
|
|
return fail(details, `${url}: ${details.statusCode} ${details.statusText}`);
|
|
}
|
|
details.content = this.response;
|
|
resolve(details);
|
|
};
|
|
|
|
const onErrorEvent = function() {
|
|
cleanup();
|
|
fail({ url }, errorCantConnectTo.replace('{{msg}}', url));
|
|
};
|
|
|
|
const onTimeout = function() {
|
|
xhr.abort();
|
|
};
|
|
|
|
// https://github.com/gorhill/uBlock/issues/2526
|
|
// - Timeout only when there is no progress.
|
|
const onProgressEvent = function(ev) {
|
|
if ( ev.loaded === contentLoaded ) { return; }
|
|
contentLoaded = ev.loaded;
|
|
if ( timeoutTimer !== undefined ) {
|
|
clearTimeout(timeoutTimer);
|
|
}
|
|
timeoutTimer = vAPI.setTimeout(onTimeout, timeoutAfter);
|
|
};
|
|
|
|
// Be ready for thrown exceptions:
|
|
// I am pretty sure it used to work, but now using a URL such as
|
|
// `file:///` on Chromium 40 results in an exception being thrown.
|
|
try {
|
|
xhr.open('get', url, true);
|
|
xhr.addEventListener('load', onLoadEvent);
|
|
xhr.addEventListener('error', onErrorEvent);
|
|
xhr.addEventListener('abort', onErrorEvent);
|
|
xhr.addEventListener('progress', onProgressEvent);
|
|
xhr.responseType = options.responseType || 'text';
|
|
xhr.send();
|
|
timeoutTimer = vAPI.setTimeout(onTimeout, timeoutAfter);
|
|
} catch (e) {
|
|
onErrorEvent.call(xhr);
|
|
}
|
|
|
|
// End of executor
|
|
});
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
assets.fetchText = async function(url) {
|
|
const isExternal = reIsExternalPath.test(url);
|
|
let actualUrl = isExternal ? url : vAPI.getURL(url);
|
|
|
|
// https://github.com/gorhill/uBlock/issues/2592
|
|
// Force browser cache to be bypassed, but only for resources which have
|
|
// been fetched more than one hour ago.
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/682#issuecomment-515197130
|
|
// Provide filter list authors a way to completely bypass
|
|
// the browser cache.
|
|
// https://github.com/gorhill/uBlock/commit/048bfd251c9b#r37972005
|
|
// Use modulo prime numbers to avoid generating the same token at the
|
|
// same time across different days.
|
|
// Do not bypass browser cache if we are asked to be gentle on remote
|
|
// servers.
|
|
if ( isExternal && remoteServerFriendly !== true ) {
|
|
const cacheBypassToken =
|
|
µb.hiddenSettings.updateAssetBypassBrowserCache
|
|
? Math.floor(Date.now() / 1000) % 86413
|
|
: Math.floor(Date.now() / 3600000) % 13;
|
|
const queryValue = `_=${cacheBypassToken}`;
|
|
if ( actualUrl.indexOf('?') === -1 ) {
|
|
actualUrl += '?';
|
|
} else {
|
|
actualUrl += '&';
|
|
}
|
|
actualUrl += queryValue;
|
|
}
|
|
|
|
let details = { content: '' };
|
|
try {
|
|
details = await assets.fetch(actualUrl);
|
|
|
|
// Consider an empty result to be an error
|
|
if ( stringIsNotEmpty(details.content) === false ) {
|
|
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 for example.
|
|
const text = details.content.trim();
|
|
if ( text.startsWith('<') && text.endsWith('>') ) {
|
|
details.content = '';
|
|
}
|
|
|
|
// Important: Non empty text resource must always end with a newline
|
|
if (
|
|
details.content.length !== 0 &&
|
|
details.content.endsWith('\n') === false
|
|
) {
|
|
details.content += '\n';
|
|
}
|
|
} 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;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// https://github.com/gorhill/uBlock/issues/3331
|
|
// Support the seamless loading of sublists.
|
|
|
|
assets.fetchFilterList = async function(mainlistURL) {
|
|
const toParsedURL = url => {
|
|
try {
|
|
return new URL(url.trim());
|
|
} 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).
|
|
let rootDirectoryURL = toParsedURL(
|
|
reIsExternalPath.test(mainlistURL)
|
|
? mainlistURL
|
|
: vAPI.getURL(mainlistURL)
|
|
);
|
|
if ( rootDirectoryURL !== undefined ) {
|
|
const pos = rootDirectoryURL.pathname.lastIndexOf('/');
|
|
if ( pos !== -1 ) {
|
|
rootDirectoryURL.pathname =
|
|
rootDirectoryURL.pathname.slice(0, pos + 1);
|
|
} else {
|
|
rootDirectoryURL = undefined;
|
|
}
|
|
}
|
|
|
|
const sublistURLs = new Set();
|
|
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/1113
|
|
// Process only `!#include` directives which are not excluded by an
|
|
// `!#if` directive.
|
|
const processIncludeDirectives = function(results) {
|
|
const out = [];
|
|
const reInclude = /^!#include +(\S+)[^\n\r]*(?:[\n\r]+|$)/gm;
|
|
for ( const result of results ) {
|
|
if ( typeof result === 'string' ) {
|
|
out.push(result);
|
|
continue;
|
|
}
|
|
if ( result instanceof Object === false ) { continue; }
|
|
const content = result.content;
|
|
const slices = µb.preparseDirectives.split(content);
|
|
for ( let i = 0, n = slices.length - 1; i < n; i++ ) {
|
|
const slice = content.slice(slices[i+0], slices[i+1]);
|
|
if ( (i & 1) !== 0 ) {
|
|
out.push(slice);
|
|
continue;
|
|
}
|
|
let lastIndex = 0;
|
|
for (;;) {
|
|
if ( rootDirectoryURL === undefined ) { break; }
|
|
const match = reInclude.exec(slice);
|
|
if ( match === null ) { break; }
|
|
if ( toParsedURL(match[1]) !== undefined ) { continue; }
|
|
if ( match[1].indexOf('..') !== -1 ) { continue; }
|
|
// Compute nested list path relative to parent list path
|
|
const pos = result.url.lastIndexOf('/');
|
|
if ( pos === -1 ) { continue; }
|
|
const subURL = result.url.slice(0, pos + 1) + match[1].trim();
|
|
if ( sublistURLs.has(subURL) ) { continue; }
|
|
sublistURLs.add(subURL);
|
|
out.push(
|
|
slice.slice(lastIndex, match.index + match[0].length),
|
|
`! >>>>>>>> ${subURL}\n`,
|
|
assets.fetchText(subURL),
|
|
`! <<<<<<<< ${subURL}\n`
|
|
);
|
|
lastIndex = reInclude.lastIndex;
|
|
}
|
|
out.push(lastIndex === 0 ? slice : slice.slice(lastIndex));
|
|
}
|
|
}
|
|
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.
|
|
|
|
let allParts = [
|
|
this.fetchText(mainlistURL)
|
|
];
|
|
// Abort processing `include` directives if at least one included sublist
|
|
// can't be fetched.
|
|
do {
|
|
allParts = await Promise.all(allParts);
|
|
const part = allParts.find(part => {
|
|
return typeof part === 'object' && part.error !== undefined;
|
|
});
|
|
if ( part !== undefined ) {
|
|
return { url: mainlistURL, content: '', error: part.error };
|
|
}
|
|
allParts = processIncludeDirectives(allParts);
|
|
} while ( allParts.some(part => typeof part !== 'string') );
|
|
// If we reach this point, this means all fetches were successful.
|
|
return {
|
|
url: mainlistURL,
|
|
content: allParts.length === 1
|
|
? allParts[0]
|
|
: allParts.join('') + '\n'
|
|
};
|
|
};
|
|
|
|
/*******************************************************************************
|
|
|
|
The purpose of the asset source registry is to keep key detail information
|
|
about an asset:
|
|
- Where to load it from: this may consist of one or more URLs, either local
|
|
or remote.
|
|
- After how many days an asset should be deemed obsolete -- i.e. in need of
|
|
an update.
|
|
- The origin and type of an asset.
|
|
- The last time an asset was registered.
|
|
|
|
**/
|
|
|
|
let assetSourceRegistryPromise,
|
|
assetSourceRegistry = Object.create(null);
|
|
|
|
const getAssetSourceRegistry = function() {
|
|
if ( assetSourceRegistryPromise === undefined ) {
|
|
assetSourceRegistryPromise = cacheStorage.get(
|
|
'assetSourceRegistry'
|
|
).then(bin => {
|
|
if (
|
|
bin instanceof Object &&
|
|
bin.assetSourceRegistry instanceof Object
|
|
) {
|
|
assetSourceRegistry = bin.assetSourceRegistry;
|
|
return assetSourceRegistry;
|
|
}
|
|
return assets.fetchText(
|
|
µb.assetsBootstrapLocation || 'assets/assets.json'
|
|
).then(details => {
|
|
return details.content !== ''
|
|
? details
|
|
: assets.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 ) {
|
|
if ( dict.hasOwnProperty(prop) === false ) { continue; }
|
|
if ( dict[prop] === undefined ) {
|
|
delete entry[prop];
|
|
} else {
|
|
entry[prop] = dict[prop];
|
|
}
|
|
}
|
|
let contentURL = dict.contentURL;
|
|
if ( contentURL !== undefined ) {
|
|
if ( typeof contentURL === 'string' ) {
|
|
contentURL = entry.contentURL = [ contentURL ];
|
|
} else if ( Array.isArray(contentURL) === false ) {
|
|
contentURL = entry.contentURL = [];
|
|
}
|
|
let remoteURLCount = 0;
|
|
for ( let i = 0; i < contentURL.length; i++ ) {
|
|
if ( reIsExternalPath.test(contentURL[i]) ) {
|
|
remoteURLCount += 1;
|
|
}
|
|
}
|
|
entry.hasLocalURL = remoteURLCount !== contentURL.length;
|
|
entry.hasRemoteURL = remoteURLCount !== 0;
|
|
} else if ( entry.contentURL === undefined ) {
|
|
entry.contentURL = [];
|
|
}
|
|
if ( typeof entry.updateAfter !== 'number' ) {
|
|
entry.updateAfter = 5;
|
|
}
|
|
if ( entry.submitter ) {
|
|
entry.submitTime = Date.now(); // To detect stale entries
|
|
}
|
|
assetSourceRegistry[assetKey] = entry;
|
|
};
|
|
|
|
const unregisterAssetSource = function(assetKey) {
|
|
assetCacheRemove(assetKey);
|
|
delete assetSourceRegistry[assetKey];
|
|
};
|
|
|
|
const saveAssetSourceRegistry = (( ) => {
|
|
let timer;
|
|
const save = function() {
|
|
timer = undefined;
|
|
cacheStorage.set({ assetSourceRegistry });
|
|
};
|
|
return function(lazily) {
|
|
if ( timer !== undefined ) {
|
|
clearTimeout(timer);
|
|
}
|
|
if ( lazily ) {
|
|
timer = vAPI.setTimeout(save, 500);
|
|
} else {
|
|
save();
|
|
}
|
|
};
|
|
})();
|
|
|
|
const updateAssetSourceRegistry = function(json, silent) {
|
|
let newDict;
|
|
try {
|
|
newDict = JSON.parse(json);
|
|
} catch (ex) {
|
|
}
|
|
if ( newDict instanceof Object === false ) { return; }
|
|
|
|
const oldDict = assetSourceRegistry;
|
|
|
|
// Remove obsolete entries (only those which were built-in).
|
|
for ( const assetKey in oldDict ) {
|
|
if (
|
|
newDict[assetKey] === undefined &&
|
|
oldDict[assetKey].submitter === undefined
|
|
) {
|
|
unregisterAssetSource(assetKey);
|
|
}
|
|
}
|
|
// Add/update existing entries. Notify of new asset sources.
|
|
for ( const assetKey in newDict ) {
|
|
if ( oldDict[assetKey] === undefined && !silent ) {
|
|
fireNotification(
|
|
'builtin-asset-source-added',
|
|
{ assetKey: assetKey, entry: newDict[assetKey] }
|
|
);
|
|
}
|
|
registerAssetSource(assetKey, newDict[assetKey]);
|
|
}
|
|
saveAssetSourceRegistry();
|
|
};
|
|
|
|
assets.registerAssetSource = async function(assetKey, details) {
|
|
await getAssetSourceRegistry();
|
|
registerAssetSource(assetKey, details);
|
|
saveAssetSourceRegistry(true);
|
|
};
|
|
|
|
assets.unregisterAssetSource = async function(assetKey) {
|
|
await getAssetSourceRegistry();
|
|
unregisterAssetSource(assetKey);
|
|
saveAssetSourceRegistry(true);
|
|
};
|
|
|
|
/*******************************************************************************
|
|
|
|
The purpose of the asset cache registry is to keep track of all assets
|
|
which have been persisted into the local cache.
|
|
|
|
**/
|
|
|
|
const assetCacheRegistryStartTime = Date.now();
|
|
let assetCacheRegistryPromise;
|
|
let assetCacheRegistry = {};
|
|
|
|
const getAssetCacheRegistry = function() {
|
|
if ( assetCacheRegistryPromise === undefined ) {
|
|
assetCacheRegistryPromise = cacheStorage.get(
|
|
'assetCacheRegistry'
|
|
).then(bin => {
|
|
if (
|
|
bin instanceof Object &&
|
|
bin.assetCacheRegistry instanceof Object
|
|
) {
|
|
if ( Object.keys(assetCacheRegistry).length === 0 ) {
|
|
assetCacheRegistry = bin.assetCacheRegistry;
|
|
} else {
|
|
console.error(
|
|
'getAssetCacheRegistry(): assetCacheRegistry reassigned!'
|
|
);
|
|
if (
|
|
Object.keys(bin.assetCacheRegistry).sort().join() !==
|
|
Object.keys(assetCacheRegistry).sort().join()
|
|
) {
|
|
console.error(
|
|
'getAssetCacheRegistry(): assetCacheRegistry changes overwritten!'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return assetCacheRegistry;
|
|
});
|
|
}
|
|
|
|
return assetCacheRegistryPromise;
|
|
};
|
|
|
|
const saveAssetCacheRegistry = (( ) => {
|
|
let timer;
|
|
const save = function() {
|
|
timer = undefined;
|
|
cacheStorage.set({ assetCacheRegistry });
|
|
};
|
|
return function(lazily) {
|
|
if ( timer !== undefined ) { clearTimeout(timer); }
|
|
if ( lazily ) {
|
|
timer = vAPI.setTimeout(save, 30000);
|
|
} else {
|
|
save();
|
|
}
|
|
};
|
|
})();
|
|
|
|
const assetCacheRead = async function(assetKey, updateReadTime = false) {
|
|
const t0 = Date.now();
|
|
const internalKey = `cache/${assetKey}`;
|
|
|
|
const reportBack = function(content) {
|
|
if ( content instanceof Blob ) { content = ''; }
|
|
const details = { assetKey: assetKey, content: content };
|
|
if ( content === '' ) { details.error = 'ENOTFOUND'; }
|
|
return details;
|
|
};
|
|
|
|
const [ , bin ] = await Promise.all([
|
|
getAssetCacheRegistry(),
|
|
cacheStorage.get(internalKey),
|
|
]);
|
|
|
|
µb.supportStats.maxAssetCacheWait = Math.max(
|
|
Date.now() - t0,
|
|
parseInt(µb.supportStats.maxAssetCacheWait, 10)
|
|
) + ' ms';
|
|
|
|
if (
|
|
bin instanceof Object === false ||
|
|
bin.hasOwnProperty(internalKey) === false
|
|
) {
|
|
return reportBack('');
|
|
}
|
|
|
|
const entry = assetCacheRegistry[assetKey];
|
|
if ( entry === undefined ) {
|
|
return reportBack('');
|
|
}
|
|
|
|
entry.readTime = Date.now();
|
|
if ( updateReadTime ) {
|
|
saveAssetCacheRegistry(true);
|
|
}
|
|
|
|
return reportBack(bin[internalKey]);
|
|
};
|
|
|
|
const assetCacheWrite = async function(assetKey, details) {
|
|
let content = '';
|
|
let options = {};
|
|
if ( typeof details === 'string' ) {
|
|
content = details;
|
|
} else if ( details instanceof Object ) {
|
|
content = details.content || '';
|
|
options = details;
|
|
}
|
|
|
|
if ( content === '' ) {
|
|
return assetCacheRemove(assetKey);
|
|
}
|
|
|
|
const cacheDict = await getAssetCacheRegistry();
|
|
|
|
let entry = cacheDict[assetKey];
|
|
if ( entry === undefined ) {
|
|
entry = cacheDict[assetKey] = {};
|
|
}
|
|
entry.writeTime = entry.readTime = Date.now();
|
|
if ( typeof options.url === 'string' ) {
|
|
entry.remoteURL = options.url;
|
|
}
|
|
cacheStorage.set({
|
|
assetCacheRegistry,
|
|
[`cache/${assetKey}`]: content
|
|
});
|
|
|
|
const result = { assetKey, content };
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/248
|
|
if ( options.silent !== true ) {
|
|
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 ) {
|
|
await Promise.all([
|
|
cacheStorage.remove(removedContent),
|
|
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 ) {
|
|
cacheStorage.set({ assetCacheRegistry });
|
|
}
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const stringIsNotEmpty = function(s) {
|
|
return typeof s === 'string' && s !== '';
|
|
};
|
|
|
|
/*******************************************************************************
|
|
|
|
User assets are NOT persisted in the cache storage. User assets are
|
|
recognized by the asset key which always starts with 'user-'.
|
|
|
|
TODO(seamless migration):
|
|
Can remove instances of old user asset keys when I am confident all users
|
|
are using uBO v1.11 and beyond.
|
|
|
|
**/
|
|
|
|
/*******************************************************************************
|
|
|
|
User assets are NOT persisted in the cache storage. User assets are
|
|
recognized by the asset key which always starts with 'user-'.
|
|
|
|
**/
|
|
|
|
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) {
|
|
return vAPI.storage.set({ [assetKey]: content }).then(( ) => {
|
|
return { assetKey, content };
|
|
});
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
assets.get = async function(assetKey, options = {}) {
|
|
if ( assetKey === µb.userFiltersPath ) {
|
|
return readUserAsset(assetKey);
|
|
}
|
|
|
|
let assetDetails = {};
|
|
|
|
const reportBack = (content, url = '', err = undefined) => {
|
|
const details = { assetKey, content };
|
|
if ( err !== undefined ) {
|
|
details.error = assetDetails.lastError = err;
|
|
} else {
|
|
assetDetails.lastError = undefined;
|
|
}
|
|
if ( options.needSourceURL ) {
|
|
if (
|
|
url === '' &&
|
|
assetCacheRegistry instanceof Object &&
|
|
assetCacheRegistry[assetKey] instanceof Object
|
|
) {
|
|
details.sourceURL = assetCacheRegistry[assetKey].remoteURL;
|
|
}
|
|
if ( reIsExternalPath.test(url) ) {
|
|
details.sourceURL = url;
|
|
}
|
|
}
|
|
return details;
|
|
};
|
|
|
|
// Skip read-time property for non-updatable assets: the property is
|
|
// completely unused for such assets and thus there is no point incurring
|
|
// storage write overhead at launch when reading compiled or selfie assets.
|
|
const updateReadTime = /^(?:compiled|selfie)\//.test(assetKey) === false;
|
|
|
|
const details = await assetCacheRead(assetKey, updateReadTime);
|
|
if ( details.content !== '' ) {
|
|
return reportBack(details.content);
|
|
}
|
|
|
|
const assetRegistry = await getAssetSourceRegistry();
|
|
assetDetails = assetRegistry[assetKey] || {};
|
|
const contentURLs = [];
|
|
if ( typeof assetDetails.contentURL === 'string' ) {
|
|
contentURLs.push(assetDetails.contentURL);
|
|
} else if ( Array.isArray(assetDetails.contentURL) ) {
|
|
contentURLs.push(...assetDetails.contentURL);
|
|
} else if ( reIsExternalPath.test(assetKey) ) {
|
|
assetDetails.content = 'filters';
|
|
contentURLs.push(assetKey);
|
|
}
|
|
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/1566#issuecomment-826473517
|
|
// Use CDN URLs as fall back URLs.
|
|
if ( Array.isArray(assetDetails.cdnURLs) ) {
|
|
contentURLs.push(...assetDetails.cdnURLs);
|
|
}
|
|
|
|
for ( const contentURL of contentURLs ) {
|
|
if ( reIsExternalPath.test(contentURL) && assetDetails.hasLocalURL ) {
|
|
continue;
|
|
}
|
|
const details = assetDetails.content === 'filters'
|
|
? await assets.fetchFilterList(contentURL)
|
|
: await assets.fetchText(contentURL);
|
|
if ( details.content === '' ) { continue; }
|
|
if ( reIsExternalPath.test(contentURL) && options.dontCache !== true ) {
|
|
assetCacheWrite(assetKey, {
|
|
content: details.content,
|
|
url: contentURL,
|
|
silent: options.silent === true,
|
|
});
|
|
}
|
|
return reportBack(details.content, contentURL);
|
|
}
|
|
return reportBack('', '', 'ENOTFOUND');
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
const getRemote = async function(assetKey) {
|
|
const assetRegistry = await getAssetSourceRegistry();
|
|
const assetDetails = assetRegistry[assetKey] || {};
|
|
|
|
const reportBack = function(content, err) {
|
|
const details = { assetKey: assetKey, content: content };
|
|
if ( err ) {
|
|
details.error = assetDetails.lastError = err;
|
|
} else {
|
|
assetDetails.lastError = undefined;
|
|
}
|
|
return details;
|
|
};
|
|
|
|
const contentURLs = [];
|
|
if ( typeof assetDetails.contentURL === 'string' ) {
|
|
contentURLs.push(assetDetails.contentURL);
|
|
} else if ( Array.isArray(assetDetails.contentURL) ) {
|
|
contentURLs.push(...assetDetails.contentURL);
|
|
}
|
|
|
|
// If asked to be gentle on remote servers, favour using dedicated CDN
|
|
// servers. If more than one CDN server is present, randomly shuffle the
|
|
// set of servers so as to spread the bandwidth burden.
|
|
//
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/1566#issuecomment-826473517
|
|
// In case of manual update, use CDNs URLs as fall back URLs.
|
|
if ( Array.isArray(assetDetails.cdnURLs) ) {
|
|
const cdnURLs = assetDetails.cdnURLs.slice();
|
|
for ( let i = 0, n = cdnURLs.length; i < n; i++ ) {
|
|
const j = Math.floor(Math.random() * n);
|
|
if ( j === i ) { continue; }
|
|
[ cdnURLs[j], cdnURLs[i] ] = [ cdnURLs[i], cdnURLs[j] ];
|
|
}
|
|
if ( remoteServerFriendly ) {
|
|
contentURLs.unshift(...cdnURLs);
|
|
} else {
|
|
contentURLs.push(...cdnURLs);
|
|
}
|
|
}
|
|
|
|
for ( const contentURL of contentURLs ) {
|
|
if ( reIsExternalPath.test(contentURL) === false ) { continue; }
|
|
|
|
const result = assetDetails.content === 'filters'
|
|
? await assets.fetchFilterList(contentURL)
|
|
: await assets.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;
|
|
}
|
|
|
|
// Success
|
|
assetCacheWrite(assetKey, {
|
|
content: result.content,
|
|
url: contentURL
|
|
});
|
|
registerAssetSource(assetKey, { error: undefined });
|
|
return reportBack(result.content);
|
|
}
|
|
|
|
return reportBack('', 'ENOTFOUND');
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
assets.put = async function(assetKey, content) {
|
|
return reIsUserAsset.test(assetKey)
|
|
? await saveUserAsset(assetKey, content)
|
|
: await assetCacheWrite(assetKey, content);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
assets.metadata = async function() {
|
|
await Promise.all([
|
|
getAssetSourceRegistry(),
|
|
getAssetCacheRegistry(),
|
|
]);
|
|
|
|
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 (
|
|
assetEntry.content === 'filters' &&
|
|
assetEntry.external !== true
|
|
) {
|
|
assetEntry.isDefault =
|
|
assetEntry.off === undefined ||
|
|
assetEntry.off === true &&
|
|
µb.listMatchesEnvironment(assetEntry);
|
|
}
|
|
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;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
assets.purge = assetCacheMarkAsDirty;
|
|
|
|
assets.remove = function(pattern) {
|
|
return assetCacheRemove(pattern);
|
|
};
|
|
|
|
assets.rmrf = function() {
|
|
return assetCacheRemove(/./);
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
// Asset updater area.
|
|
const updaterAssetDelayDefault = 120000;
|
|
const updaterUpdated = [];
|
|
const updaterFetched = new Set();
|
|
|
|
let updaterStatus;
|
|
let updaterTimer;
|
|
let updaterAssetDelay = updaterAssetDelayDefault;
|
|
let updaterAuto = false;
|
|
|
|
const updateFirst = function() {
|
|
updaterStatus = 'updating';
|
|
updaterFetched.clear();
|
|
updaterUpdated.length = 0;
|
|
fireNotification('before-assets-updated');
|
|
updateNext();
|
|
};
|
|
|
|
const updateNext = async function() {
|
|
const [ assetDict, cacheDict ] = await Promise.all([
|
|
getAssetSourceRegistry(),
|
|
getAssetCacheRegistry(),
|
|
]);
|
|
|
|
const now = Date.now();
|
|
const toUpdate = [];
|
|
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 instanceof Object) &&
|
|
(cacheEntry.writeTime + assetEntry.updateAfter * 86400000) > now
|
|
) {
|
|
continue;
|
|
}
|
|
if (
|
|
fireNotification('before-asset-updated', {
|
|
assetKey,
|
|
type: assetEntry.content
|
|
}) === true
|
|
) {
|
|
toUpdate.push(assetKey);
|
|
continue;
|
|
}
|
|
// This will remove a cached asset when it's no longer in use.
|
|
if ( cacheEntry && cacheEntry.readTime < assetCacheRegistryStartTime ) {
|
|
assetCacheRemove(assetKey);
|
|
}
|
|
}
|
|
if ( toUpdate.length === 0 ) {
|
|
return updateDone();
|
|
}
|
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/1165
|
|
// Update most obsolete asset first.
|
|
toUpdate.sort((a, b) => {
|
|
const ta = cacheDict[a] !== undefined ? cacheDict[a].writeTime : 0;
|
|
const tb = cacheDict[b] !== undefined ? cacheDict[b].writeTime : 0;
|
|
return ta - tb;
|
|
});
|
|
updaterFetched.add(toUpdate[0]);
|
|
|
|
// In auto-update context, be gentle on remote servers.
|
|
remoteServerFriendly = updaterAuto;
|
|
|
|
const result = await getRemote(toUpdate[0]);
|
|
|
|
remoteServerFriendly = false;
|
|
|
|
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() {
|
|
const assetKeys = updaterUpdated.slice(0);
|
|
updaterFetched.clear();
|
|
updaterUpdated.length = 0;
|
|
updaterStatus = undefined;
|
|
updaterAssetDelay = updaterAssetDelayDefault;
|
|
fireNotification('after-assets-updated', { assetKeys: assetKeys });
|
|
};
|
|
|
|
assets.updateStart = function(details) {
|
|
const oldUpdateDelay = updaterAssetDelay;
|
|
const newUpdateDelay = typeof details.delay === 'number'
|
|
? details.delay
|
|
: updaterAssetDelayDefault;
|
|
updaterAssetDelay = Math.min(oldUpdateDelay, newUpdateDelay);
|
|
updaterAuto = details.auto === true;
|
|
if ( updaterStatus !== undefined ) {
|
|
if ( newUpdateDelay < oldUpdateDelay ) {
|
|
clearTimeout(updaterTimer);
|
|
updaterTimer = vAPI.setTimeout(updateNext, updaterAssetDelay);
|
|
}
|
|
return;
|
|
}
|
|
updateFirst();
|
|
};
|
|
|
|
assets.updateStop = function() {
|
|
if ( updaterTimer ) {
|
|
clearTimeout(updaterTimer);
|
|
updaterTimer = undefined;
|
|
}
|
|
if ( updaterStatus !== undefined ) {
|
|
updateDone();
|
|
}
|
|
};
|
|
|
|
assets.isUpdating = function() {
|
|
return updaterStatus === 'updating' &&
|
|
updaterAssetDelay <= µb.hiddenSettings.manualUpdateAssetFetchPeriod;
|
|
};
|
|
|
|
/******************************************************************************/
|
|
|
|
export default assets;
|
|
|
|
/******************************************************************************/
|