/******************************************************************************* ηMatrix - a browser extension to black/white list requests. Copyright (C) 2014-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'; (function () { Cu.import('chrome://ematrix/content/lib/UriTools.jsm'); Cu.import('chrome://ematrix/content/lib/Tools.jsm'); Cu.import('chrome://ematrix/content/lib/RowSnapshot.jsm'); let ηm = ηMatrix; // I personally find this method better than a giant (or multiple // giant) switch statement(s), but all things considered it's not // as nice as when the language has proper macros. The idea is to // make a more flexible system, at the cost of a slightly more // verbose code and a bit more memory usage due to the new // function objects. // Method tables. // "Early" is for those methods that do not call the callback. let methods = {}; let methodsEarly = {}; // The message queue callback. let dispatchMethod = function (request, sender, cb) { let f = methods[request.what]; let early = false; if (!f) { f = methodsEarly[request.what]; if (!f) { return vAPI.messaging.UNHANDLED; } early = true; } let response = f(request, sender, cb); if (early === true) { return response; } cb(response); }; // Used here to add dispatchable functions let addMethod = function (name, f, /* &optional */ early) { if (early) { methodsEarly[name] = f; } else { methods[name] = f; } } // Default addMethod('getAssetContent', function (request, sender, cb) { ηm.assets.get(request.url, { dontCache: true, }, cb); return undefined; }, true); addMethod('selectHostsFiles', function (request, sender, cb) { ηm.selectHostsFiles(request, cb); return undefined; }, true); addMethod('forceReloadTab', function (request, sender, cb) { ηm.forceReload(request.tabId, request.bypassCache); return undefined; }); addMethod('forceUpdateAssets', function (request, sender, cb) { ηm.scheduleAssetUpdater(0); ηm.assets.updateStart({ delay: 1000, }); return undefined; }); addMethod('getUserSettings', function (request, sender, cb) { let pmat = ηm.pMatrix; return { userSettings: ηm.userSettings, matrixSwitches: { 'https-strict': (pmat.evaluateSwitch('https-strict', '*') === 1), 'referrer-spoof': (pmat.evaluateSwitch('referrer-spoof', '*') === 1), 'noscript-spoof': (pmat.evaluateSwitch('noscript-spoof', '*') === 1), }, }; }); addMethod('gotoExtensionURL', function (request, sender, cb) { Tools.gotoExtensionURL({ api: vAPI, details: request, matrix: ηm, }); return undefined; }); addMethod('gotoURL', function (request, sender, cb) { Tools.gotoURL({ api: vAPI, details: request, }); return undefined; }); addMethod('mustBlock', function (request, sender, cb) { return ηm.mustBlock(request.scope, request.hostname, request.type); }); addMethod('readRawSettings', function (request, sender, cb) { return ηm.stringFromRawSettings(); }); addMethod('writeRawSettings', function (request, sender, cb) { ηm.rawSettingsFromString(request.content); return undefined; }); addMethod('reloadHostsFiles', function (request, sender, cb) { ηm.reloadHostsFiles(); return undefined; }); addMethod('setMatrixSwitch', function (request, sender, cb) { ηm.tMatrix.setSwitch(request.switchName, '*', request.state); if (ηm.pMatrix.setSwitch(request.switchName, '*', request.state)) { ηm.saveMatrix(); } return undefined; }); addMethod('userSettings', function (request, sender, cb) { if (request.hasOwnProperty('value') === false) { request.value = undefined; } return ηm.changeUserSettings(request.name, request.value); }); vAPI.messaging.setup(dispatchMethod); // popup.js begins here addMethod('matrixSnapshot', function (request, sender, cb) { let snapShot = function (store, details) { let user = ηm.userSettings; let indices = ηm.Matrix.columnHeaderIndices; let r = { appVersion: vAPI.app.version, blockedCount: store.requestStats.blocked.all, collapseAllDomains: user.popupCollapseAllDomains, collapseBlacklistedDomains: user.popupCollapseBlacklistedDomains, diff: [], domain: store.pageDomain, has3pReferrer: store.has3pReferrer, hasMixedContent: store.hasMixedContent, hasNoscriptTags: store.hasNoscriptTags, hasWebWorkers: store.hasWebWorkers, headerIndices: Array.from(indices), hostname: store.pageHostname, mtxContentModified: (store.mtxContentModifiedTime !== details.mtxContentModifiedTime), mtxCountModified: (store.mtxCountModifiedTime !== details.mtxCountModifiedTime), mtxContentModifiedTime: store.mtxContentModifiedTime, mtxCountModifiedTime: store.mtxCountModifiedTime, pMatrixModified: (ηm.pMatrix.modifiedTime !== details.pMatrixModifiedTime), pMatrixModifiedTime: ηm.pMatrix.modifiedTime, pSwitches: {}, rows: {}, rowCount: 0, scope: '*', tabId: store.tabId, tMatrixModified: (ηm.tMatrix.modifiedTime !== details.tMatrixModifiedTime), tMatrixModifiedTime: ηm.tMatrix.modifiedTime, tSwitches: {}, url: store.pageUrl, userSettings: { colorBlindFriendly: user.colorBlindFriendly, displayTextSize: user.displayTextSize, popupScopeLevel: user.popupScopeLevel, }, }; if (typeof details.scope === 'string') { r.scope = details.scope; } else if (user.popupScopeLevel === 'site') { r.scope = r.hostname; } else if (user.popupScopeLevel === 'domain') { r.scope = r.domain; } for (let s of ηm.Matrix.switchNames) { r.tSwitches[s] = ηm.tMatrix.evaluateSwitchZ(s, r.scope); r.pSwitches[s] = ηm.pMatrix.evaluateSwitchZ(s, r.scope); } r.rows['*'] = new RowSnapshot(ηm, r.scope, '*', '*'); r.rows['1st-party'] = new RowSnapshot(ηm, r.scope, '1st-party', '1st-party'); r.rowCount += 1; let any = indices.get('*'); for (let e of store.hostnameTypeCells) { let pos = e[0].indexOf(' '); let reqhn = e[0].slice(0, pos); let reqt = e[0].slice(pos+1); // Hostname can be empty in some cases // (reported from uMatrix) if (reqhn === '') { reqhn = store.pageHostname; } let reqd = UriTools.domainFromHostname(reqhn) || reqhn; let desthn = reqhn; while (true) { if (r.rows.hasOwnProperty(desthn) !== false) { break; } r.rows[desthn] = new RowSnapshot(ηm, r.scope, desthn, reqd); r.rowCount += 1; if (desthn === reqd) { break; } pos = desthn.indexOf('.'); if (pos === -1) { break; } desthn = desthn.slice(pos + 1); } let count = e[1].size; let tindex = indices.get(reqt); let row = r.rows[reqhn]; row.counts[tindex] += count; row.counts[any] += count; row = r.rows[reqd]; row.totals[tindex] += count; row.totals[any] += count; row = r.rows['*']; row.totals[tindex] += count; row.totals[any] += count; } r.diff = ηm.tMatrix.diff(ηm.pMatrix, r.hostname, Object.keys(r.rows)); return r; }; let snapFromId = function (details, cb) { let snapIf = function (id, details) { let store = ηm.pageStoreFromTabId(id); if (store === null) { cb('ENOTFOUND'); return; } let tmat = ηm.tMatrix; let pmat = ηm.pMatrix; let mtx = details.mtxContentModifiedTime; if (tmat.modifiedTime === details.tMatrixModifiedTime && pmat.modifiedTime === details.pMatrixModifiedTime && store.mtxContentModifiedTime === mtx && store.mtxCountModifiedTime === mtx) { cb('ENOCHANGE'); return; } cb(snapShot(store, details)); }; if (details.tabId) { snapIf(details.tabId, details); return; } let onReady = function (tab) { if (tab instanceof Object === false) { cb('ENOTFOUND'); return; } let id = tab.url.lastIndexOf(vAPI.getURL('dashboard.html'), 0) !== 0 ? tab.id : vAPI.noTabId; snapIf(id, details); }; vAPI.tabs.get(null, onReady); }; snapFromId(request, cb); return undefined; }, true); addMethod('toggleMatrixSwitch', function (request, sender, cb) { let sz = ηm.tMatrix.evaluateSwitchZ(request.switchName, request.srcHostname); ηm.tMatrix.setSwitchZ(request.switchName, request.srcHostname, sz === false); return undefined; }); addMethod('blacklistMatrixCell', function (request, sender, cb) { ηm.tMatrix.blacklistCell(request.srcHostname, request.desHostname, request.type); return undefined; }); addMethod('whitelistMatrixCell', function (request, sender, cb) { ηm.tMatrix.whitelistCell(request.srcHostname, request.desHostname, request.type); return undefined; }); addMethod('graylistMatrixCell', function (request, sender, cb) { ηm.tMatrix.graylistCell(request.srcHostname, request.desHostname, request.type); return undefined; }); addMethod('applyDiffToPermanentMatrix', function (request, sender, cb) { if (ηm.pMatrix.applyDiff(request.diff, ηm.tMatrix)) { ηm.saveMatrix(); } return undefined; }); addMethod('applyDiffToTemporaryMatrix', function (request, sender, cb) { ηm.tMatrix.applyDiff(request.diff, ηm.pMatrix); return undefined; }); addMethod('revertTemporaryMatrix', function (request, sender, cb) { ηm.tMatrix.assign(ηm.pMatrix); return undefined; }); vAPI.messaging.listen('popup.js', dispatchMethod); // Content scripts handling begins here let placeholders = undefined; let phReadTime = 0; addMethod('contentScriptHasLocalStorage', function (request, sender, cb) { let contentScriptLocalStorageHandler = function (id, originURL) { let tabContext = ηm.tabContextManager.lookup(id); if (tabContext === null) { return; } let blocked = ηm.mustBlock(tabContext.rootHostname, UriTools.hostnameFromURI(originURL), 'cookie'); let store = ηm.pageStoreFromTabId(id); if (store !== null) { let requestURL = originURL + '/{localStorage}'; store.recordRequest('cookie', requestURL, blocked); ηm.logger.writeOne(id, 'net', tabContext.rootHostname, requestURL, 'cookie', blocked); } let removeStorage = blocked && ηm.userSettings.deleteLocalStorage; if (removeStorage) { ηm.localStorageRemovedCounter++; } return removeStorage; }; let tabId = sender && sender.tab ? sender.tab.id || 0 : 0; return contentScriptLocalStorageHandler(tabId, request.originURL); }); addMethod('lookupBlockedCollapsibles', function (request, sender, cb) { let lookupBlockedCollapsibles = function (id, requests) { if (phReadTime < ηm.rawSettingsWriteTime) { placeholders = undefined; } if (placeholders === undefined) { placeholders = { frame: ηm.rawSettings.framePlaceholder, image: ηm.rawSettings.imagePlaceholder, }; if (placeholders.frame) { let doc = ηm.rawSettings.framePlaceholderDocument; let bg = ηm.rawSettings.framePlaceholderBackground; placeholders.frameDocument = doc.replace('{{bg}}', bg !== 'default' ? bg : ηm.rawSettings.placeHolderBackground); } if (placeholders.image) { placeholders.imageBorder = ηm.rawSettings.imagePlaceholderBorder !== 'default' ? ηm.rawSettings.imagePlaceholderBorder : ηm.rawSettings.placeholderBorder; placeholders.imageBackground = ηm.rawSettings.imagePlaceholderBackground !== 'default' ? ηm.rawSettings.imagePlaceholderBackground : ηm.rawSettings.placeholderBackground; } phReadTime = Date.now(); } let response = { blockedResources: [], hash: requests.hash, id: requests.id, placeholders: placeholders, }; let context = ηm.tabContextManager.lookup(id); if (context === null) { return response; } let store = ηm.pageStoreFromTabId(id); if (store !== null) { store.lookupBlockedCollapsibles(requests, response); } return response; }; let tabId = sender && sender.tab ? sender.tab.id || 0 : 0; return lookupBlockedCollapsibles(tabId, request); }); addMethod('mustRenderNoscriptTags?', function (request, sender, cb) { let tabId = sender && sender.tab ? sender.tab.id || 0 : 0; let tabContext = ηm.tabContextManager.lookup(tabId); let rootHostname = tabContext && tabContext.rootHostname; let pageStore = ηm.pageStoreFromTabId(tabId); if (tabContext === null) { return undefined; } let response = ηm.tMatrix.mustBlock(rootHostname, rootHostname, 'script') && ηm.tMatrix.evaluateSwitchZ('noscript-spoof', rootHostname); if (pageStore !== null) { pageStore.hasNoscriptTags = true; } // https://github.com/gorhill/uMatrix/issues/225 // A good place to force an update of the page title, as // at this point the DOM has been loaded. ηm.updateTitle(tabId); return response; }); addMethod('securityPolicyViolation', function (request, sender, cb) { let foundInlineCode = function (id, store, details, type) { if (store === null) { return; } let pageHostname = store.pageHostname; let uri = UriTools.set(details.documentURI); let frameURL = UriTools.normalizedURI(); let blocked = details.blocked; if (blocked === undefined) { blocked = ηm.mustBlock(pageHostname, uri.hostname, type); } let mapTo = { css: 'style', script: 'script', }; // https://github.com/gorhill/httpswitchboard/issues/333 // Look-up here whether inline scripting is blocked for the frame. let url = frameURL + '{inline_' + mapTo[type] + '}'; store.recordRequest(type, url, blocked); ηm.logger.writeOne(id, 'net', pageHostname, url, type, blocked); }; let tabId = sender && sender.tab ? sender.tab.id || 0 : 0; let tabContext = ηm.tabContextManager.lookup(tabId); let rootHostname = tabContext && tabContext.rootHostname; let pageStore = ηm.pageStoreFromTabId(tabId); if (request.directive === 'worker-src') { let url = UriTools.hostnameFromURI(request.blockedURI) !== '' ? request.blockedURI : request.documentURI; if (pageStore !== null) { pageStore.hasWebWorkers = true; pageStore.recordRequest('script', url, request.blocked); } if (tabContext !== null) { ηm.logger.writeOne(tabId, 'net', rootHostname, url, 'worker', request.blocked); } } else if (request.directive === 'script-src') { foundInlineCode(tabId, pageStore, request, 'script'); } else if (request.directive === 'style-src') { foundInlineCode(tabId, pageStore, request, 'css'); } return undefined; }); addMethod('shutdown?', function (request, sender, cb) { let tabId = sender && sender.tab ? sender.tab.id || 0 : 0; let tabContext = ηm.tabContextManager.lookup(tabId); let rootHostname = tabContext && tabContext.rootHostname; let response = undefined; if (tabContext !== null) { response = ηm.tMatrix.evaluateSwitchZ('matrixb-off', rootHostname); } return response; }); vAPI.messaging.listen('contentscript.js', dispatchMethod); // cloud-ui.js begins here addMethod('cloudGetOptions', function (request, sender, cb) { vAPI.cloud.getOptions(function (options) { options.enabled = ηm.userSettings.cloudStorageEnabled === true; cb(options); }); return undefined; }, true); addMethod('cloudSetOptions', function (request, sender, cb) { vAPI.cloud.setOptions(request.options, cb); return undefined; }, true); addMethod('cloudPull', function (request, sender, cb) { return vAPI.cloud.pull(request.datakey, cb); }, true); addMethod('cloudPush', function (request, sender, cb) { return vAPI.cloud.push(request.datakey, request.data, cb); }, true); vAPI.messaging.listen('cloud-ui.js', dispatchMethod); // user-rules.js begins here addMethod('getUserRules', function (request, sender, cb) { return { temporaryRules: ηm.tMatrix.toString(), permanentRules: ηm.pMatrix.toString(), }; }); addMethod('setUserRules', function (request, sender, cb) { if (typeof request.temporaryRules === 'string') { ηm.tMatrix.fromString(request.temporaryRules); } if (typeof request.permanentRules === 'string') { ηm.pMatrix.fromString(request.permanentRules); ηm.saveMatrix(); } return { temporaryRules: ηm.tMatrix.toString(), permanentRules: ηm.pMatrix.toString(), }; }); vAPI.messaging.listen('user-rules.js', dispatchMethod); // hosts-files.js begins here addMethod('getLists', function (request, sender, cb) { let prepare = function (entries) { for (let e in entries) { if (entries.hasOwnProperty(e) === false) { continue; } let entry = entries[e]; if (typeof entry.homeURL === 'string') { let homehost = UriTools.hostnameFromURI(entry.homeURL); entry.homeHostname = homehost entry.homeDomain = UriTools.domainFromHostname(homehost); } } }; let getLists = function (cb) { let r = { autoUpdate: ηm.userSettings.autoUpdate, available: null, cache: null, current: ηm.liveHostsFiles, blockedHostnameCount: ηm.ubiquitousBlacklist.count, }; let onMetadataReady = function (entries) { r.cache = entries; prepare(r.cache); cb(r); }; let onHostsReady = function (lists) { r.available = lists; prepare(r.available); ηm.assets.metadata(onMetadataReady); }; ηm.getAvailableHostsFiles(onHostsReady); }; return getLists(cb); }, true); addMethod('purgeCache', function (request, sender, cb) { ηm.assets.purge(request.assetKey); ηm.assets.remove('cache/' + request.assetKey); return undefined; }); addMethod('purgeAllCaches', function (request, sender, cb) { if (request.hard) { ηm.assets.remove(/./); } else { ηm.assets.purge(/./, 'public_suffix_list.dat'); } return undefined; }); vAPI.messaging.listen('hosts-files.js', dispatchMethod); // about.js begins here addMethod('getAllUserData', function (request, sender, cb) { return { app: vAPI.app.name, version: vAPI.app.version, when: Date.now(), settings: ηm.userSettings, rules: ηm.pMatrix.toString(), hostsFiles: ηm.liveHostsFiles, rawSettings: ηm.rawSettings, }; }); addMethod('getSomeStats', function (request, sender, cb) { return { version: vAPI.app.version, storageUsed: ηm.storageUsed }; }); addMethod('restoreAllUserData', function (request, sender, cb) { let restoreData = function (data) { let countdown = 4; let onCountdown = function () { countdown -= 1; if (countdown === 0) { vAPI.app.restart(); } }; let onAllRemoved = function () { vAPI.storage.set(data.settings, onCountdown); vAPI.storage.set({ userMatrix: data.rules, }, onCountdown); vAPI.storage.set({ liveHostsFiles: data.hostsFiles, }, onCountdown); if (data.rawSettings instanceof Object) { ηMatrix.saveRawSettings(data.rawSettings, onCountdown); } }; // If we are going to restore all, might as well wipe out // clean local storage ηm.XAL.keyvalRemoveAll(onAllRemoved); }; restoreData(request.userData); return undefined; }); addMethod('resetAllUserData', function (request, sender, cb) { let onAllRemoved = function () { vAPI.app.restart(); }; ηm.XAL.keyvalRemoveAll(onAllRemoved); /* Let's also clear the database, just to really make it a reset */ ηm.assets.rmrf(); return undefined; }); vAPI.messaging.listen('about.js', dispatchMethod); // logger-ui.js begins here let loggerURL = vAPI.getURL('logger-ui.html'); addMethod('readMany', function (request, sender, cb) { if (ηm.logger.ownerId !== undefined && request.ownerId !== ηm.logger.ownerId) { return { unavailable: true, }; } let tabIds = {}; for (let id in ηm.pageStores) { let store = ηm.pageStoreFromTabId(id); if (store === null) { continue; } if (store.rawUrl.startsWith(loggerURL)) { continue; } tabIds[id] = store.title || store.rawUrl; } return { colorBlind: false, entries: ηm.logger.readAll(request.ownerId), maxLoggedRequests: ηm.userSettings.maxLoggedRequests, noTabId: vAPI.noTabId, tabIds: tabIds, tabIdsToken: ηm.pageStoresToken, }; }); addMethod('releaseView', function (request, sender, cb) { if (request.ownerId === ηm.logger.ownerId) { ηm.logger.ownerId = undefined; } return undefined; }); vAPI.messaging.listen('logger-ui.js', dispatchMethod); })();