/******************************************************************************* ηMatrix - a browser extension to black/white list requests. Copyright (C) 2013-2019 Raymond Hill Copyright (C) 2019-2022 Alessio Vanni 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://gitlab.com/vannilla/ematrix uMatrix Home: https://github.com/gorhill/uMatrix */ 'use strict'; ηMatrix.assets = (function() { let api = {}; let observers = []; let externalPathRegex = /^(?:[a-z-]+):\/\//; let connectionError = vAPI.i18n('errorCantConnectTo'); let notifyObservers = function (topic, details) { let result; for (let i=0; i now) { continue; } if (notifyObservers('before-asset-updated', {assetKey: key})) { return key; } gcOne(key); } return undefined; }; let onUpdate = function (details) { if (details.content !== '') { updated.push(details.assetKey); if (details.assetKey === 'assets.json') { updateSourceRegistry(details.content); } } else { notifyObservers('asset-update-failed', { assetKey: details.assetKey, }); } if (findOne() !== undefined) { vAPI.setTimeout(updateNext, updateDelay); } else { updateEnd(); } }; let updateOne = function () { let key = findOne(); if (key === undefined) { updateEnd(); return; } updateFetch.add(key); getRemote(key, onUpdate); }; let onSourceReady = function (registry) { sourceReg = registry; updateOne(); }; let onCacheReady = function (registry) { cacheReg = registry; getSourceRegistry(onSourceReady); }; getCacheRegistry(onCacheReady); }; let updateEnd = function () { let keys = updated.slice(0); updateFetch.clear(); updateStatus = 'stop'; updateDelay = updateDefaultDelay; notifyObservers('after-asset-updated', { assetKeys: keys, }); }; // Assets API api.addObserver = function (observer) { if (observers.indexOf(observer) === -1) { observers.push(observer); } }; api.removeObserver = function (observer) { let pos = observers.indexOf(observer); while (pos !== -1) { observers.splice(pos, 1); pos = observers.indexOf(observer); } }; api.fetchText = function (url, onLoad, onError, tries) { let iurl = externalPathRegex.test(url) ? url : vAPI.getURL(url); let tr = (tries === undefined) ? 10 : tries; if (typeof onError !== 'function') { onError = onLoad; } let onResponseReceived = function () { this.onload = this.onerror = this.ontimeout = null; let details = { url: url, content: '', // On local files this.status is 0, but the request // is successful statusCode: this.status || 200, statusText: this.statusText || '', }; if (details.statusCode < 200 || details.statusCode >= 300) { return onError.call(null, details, tr); } if (isEmptyString(this.responseText) === true) { return onError.call(null, details, tr); } let t = this.responseText.trim(); // Discard HTML as it's probably an error // (the request expects plain text as a response) if (t.startsWith('<') && t.endsWith('>')) { return onError.call(null, details, tr); } details.content = t; return onLoad.call(null, details, tr); }; let onErrorReceived = function () { this.onload = this.onerror = this.ontimeout = null; ηMatrix.logger.writeOne('', 'error', connectionError.replace('{{url}}', iurl)); onError.call(null, { url: url, content: '', }, tr); }; let req = new XMLHttpRequest(); req.open('GET', iurl, true); req.timeout = 30000; req.onload = onResponseReceived; req.onerror = onErrorReceived; req.ontimeout = onErrorReceived; req.responseType = 'text'; try { // This can throw in some cases req.send(); } catch (e) { onErrorReceived.call(req); } }; api.registerAssetSource = function (key, details) { getSourceRegistry(function () { registerSource(key, details); saveSourceRegistry(true); }); }; api.unregisterAssetSource = function (key) { getSourceRegistry(function () { unregisterSource(key); saveSourceRegistry(true); }); }; api.get = function (key, options, callback) { let cb; let opt; if (typeof options === 'function') { cb = options; opt = {}; } else if (typeof callback !== 'function') { cb = noOp; opt = options; } else { cb = callback; opt = options; } let assetDetails = {}; let urls = []; let contentUrl = ''; let report = function (content, error) { let details = { assetKey: key, content: content, }; if (error) { details.error = assetDetails.error = error; } else { assetDetails.error = undefined; } cb(details); }; let onContentNotLoaded = function (details, tries) { let external; let tr = (tries === undefined) ? 10 : tries; if (tr <= 0) { console.warn('ηMatrix couldn\'t download the asset ' +assetDetails.title); return; } while ((contentUrl = urls.shift())) { external = externalPathRegex.test(contentUrl); if (external === true && assetDetails.loaded !== true) { break; } if (external === false || assetDetails.hasLocalURL !== true) { break; } } if (!contentUrl) { return report('', 'E_NOTFOUND'); } api.fetchText(contentUrl, onContentLoaded, onContentNotLoaded, tr-1); }; let onContentLoaded = function (details, tries) { if (isEmptyString(details.content) === true) { return onContentNotLoaded(undefined, tries); } if (externalPathRegex.test(details.url) && opt.dontCache !== true) { writeCache(key, { content: details.content, url: contentUrl, }); } assetDetails.loaded = true; report(details.content); }; let onCachedContentLoad = function (details) { if (details.content !== '') { return report(details.content); } let onRead = function (details) { console.debug(details); report(details.content || 'missing contents'); } let onReady = function (registry) { assetDetails = registry[key] || {}; if (typeof assetDetails.contentURL === 'string') { urls = [assetDetails.contentURL]; } else if (Array.isArray(assetDetails.contentURL)) { urls = assetDetails.contentURL.slice(0); } if (true === assetDetails.loaded) { readCache(key, onRead); } else { onContentNotLoaded(); } } getSourceRegistry(onReady); }; readCache(key, onCachedContentLoad); }; api.put = function (key, content, callback) { writeCache(key, content, callback); }; api.metadata = function (callback) { let onSourceReady = function (registry) { let source = JSON.parse(JSON.stringify(registry)); let cache = cacheRegistry; let sourceEntry; let cacheEntry; let now = Date.now(); let obsoleteAfter; for (let key in source) { sourceEntry = source[key]; cacheEntry = cache[key]; if (cacheEntry) { sourceEntry.cached = true; sourceEntry.writeTime = cacheEntry.writeTime; obsoleteAfter = cacheEntry.writeTime + sourceEntry.updateAfter * 86400000; sourceEntry.obsolete = obsoleteAfter < now; sourceEntry.remoteURL = cacheEntry.remoteURL; } else { sourceEntry.writeTime = 0; obsoleteAfter = 0; sourceEntry.obsolete = true; } } callback(source); } let onCacheReady = function () { getSourceRegistry(onSourceReady); } getCacheRegistry(onCacheReady); }; api.purge = function (pattern, exclude, callback) { markDirtyCache(pattern, exclude, callback); }; api.remove = function (pattern, callback) { removeCache(pattern, callback); }; api.rmrf = function () { removeCache(/./); }; api.updateStart = function (details) { let oldDelay = updateDelay; let newDelay = details.delay || updateDefaultDelay; updateDelay = Math.min(oldDelay, newDelay); if (updateStatus !== 'stop') { if (newDelay < oldDelay) { clearTimeout(updateTimer); updateTimer = vAPI.setTimeout(updateNext, updateDelay); } return; } updateStart(); }; api.updateStop = function () { if (updateTimer) { clearTimeout(updateTimer); updateTimer = undefined; } if (updateStatus === 'running') { updateEnd(); } }; api.checkVersion = function () { let cache; let onSourceReady = function (registry) { let source = JSON.parse(JSON.stringify(registry)); let version = ηMatrix.userSettings.assetVersion; if (!version) { ηMatrix.userSettings.assetVersion = 1; version = 1; } if (!source["assets.json"].version || version > source["assets.json"].version) { for (let key in source) { switch (key) { case "hphosts": case "malware-0": case "malware-1": delete source[key]; api.remove(key, function () {}); break; default: break; } source["assets.json"].version = version; } let createRegistry = function () { api.fetchText (ηMatrix.assetsBootstrapLocation || 'assets/assets.json', function (details) { updateSourceRegistry(details.content, true); }); }; createRegistry(); } }; let onCacheReady = function (registry) { cache = JSON.parse(JSON.stringify(registry)); getSourceRegistry(onSourceReady); }; getCacheRegistry(onCacheReady); }; return api; })();