/******************************************************************************* ηMatrix - a browser extension to black/white list requests. Copyright (C) 2013-2019 Raymond Hill Copyright (C) 2019-2020 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://libregit.spks.xyz/heckyel/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 0) { vAPI.cacheStorage.remove(removedContent); let bin = { assetCacheRegistry: cacheRegistry, }; vAPI.cacheStorage.set(bin); } if (typeof callback === 'function') { callback(); } 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); if (pos !== -1) { observers.splice(pos, 1); } }; 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) { onError.call(null, details, tr); return; } if (isEmptyString(this.responseText) === true) { onError.call(null, details, tr); return; } 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('>')) { onError.call(null, details, tr); return; } details.content = t; 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 contentUrl = undefined; 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 urls = []; let tr = (tries === undefined) ? 10 : tries; if (tr <= 0) { console.warn('ηMatrix couldn\'t download the asset ' +assetDetails.title); return; } if (typeof assetDetails.contentURL === 'string') { urls = [assetDetails.contentURL]; } else if (Array.isArray(assetDetails.contentURL)) { urls = assetDetails.contentURL.slice(0); } while ((contentUrl = urls.shift())) { external = externalPathRegex.test(contentUrl); if (external === true && assetDetails.loaded !== true) { break; } if (external === false || assetDetails.hasLocalURL !== true) { break; } } if (!contentUrl) { report('', 'E_NOTFOUND'); return; } api.fetchText(contentUrl, onContentLoaded, onContentNotLoaded, tr-1); }; let onContentLoaded = function (details, tries) { if (isEmptyString(details.content) === true) { onContentNotLoaded(undefined, tries); return; } 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 !== '') { report(details.content); return; } let onReady = function (registry) { assetDetails = registry[key] || {}; 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) { cacheRemove(pattern, callback); }; api.rmrf = function () { cacheRemove(/./); }; api.updateStart = function (details) { let oldDelay = updateDelay; let newDelay = details.delay || updateDefaultDelay; updateDelay = Math.min(oldDelay, newDelay); if (updateStatus !== undefined) { 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; })(); /******************************************************************************/