/******************************************************************************* uBlock Origin Lite - a comprehensive, MV3-compliant content blocker Copyright (C) 2022-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 */ /******************************************************************************/ import { adminRead, browser, dnr, localRead, localWrite, runtime, sessionRead, sessionWrite, } from './ext.js'; import { broadcastMessage, ubolLog, } from './utils.js'; import { defaultRulesetsFromLanguage, enableRulesets, getEnabledRulesetsDetails, getRulesetDetails, updateDynamicRules, } from './ruleset-manager.js'; import { getDefaultFilteringMode, getFilteringMode, getTrustedSites, setDefaultFilteringMode, setFilteringMode, setTrustedSites, syncWithBrowserPermissions, } from './mode-manager.js'; import { registerInjectables, } from './scripting-manager.js'; /******************************************************************************/ const rulesetConfig = { version: '', enabledRulesets: ['default'], autoReload: true, showBlockedCount: true, }; const UBOL_ORIGIN = runtime.getURL('').replace(/\/$/, ''); const canShowBlockedCount = typeof dnr.setExtensionActionOptions === 'function'; let firstRun = false; let wakeupRun = false; /******************************************************************************/ function getCurrentVersion() { return runtime.getManifest().version; } async function loadRulesetConfig() { let data = sessionRead('rulesetConfig'); if (data) { rulesetConfig.version = data.version; rulesetConfig.enabledRulesets = data.enabledRulesets; rulesetConfig.autoReload = typeof data.autoReload === 'boolean' ? data.autoReload : true; rulesetConfig.showBlockedCount = typeof data.showBlockedCount === 'boolean' ? data.showBlockedCount : true; wakeupRun = true; return; } data = localRead('rulesetConfig'); if (data) { rulesetConfig.version = data.version; rulesetConfig.enabledRulesets = data.enabledRulesets; rulesetConfig.autoReload = typeof data.autoReload === 'boolean' ? data.autoReload : true; rulesetConfig.showBlockedCount = typeof data.showBlockedCount === 'boolean' ? data.showBlockedCount : true; sessionWrite('rulesetConfig', rulesetConfig); return; } rulesetConfig.enabledRulesets = defaultRulesetsFromLanguage(); sessionWrite('rulesetConfig', rulesetConfig); localWrite('rulesetConfig', rulesetConfig); firstRun = true; } async function saveRulesetConfig() { sessionWrite('rulesetConfig', rulesetConfig); return localWrite('rulesetConfig', rulesetConfig); } /******************************************************************************/ async function hasGreatPowers(origin) { if (/^https?:\/\//.test(origin) === false) { return false; } return browser.permissions.contains({ origins: [`${origin}/*`], }); } function hasOmnipotence() { return browser.permissions.contains({ origins: [''], }); } async function onPermissionsRemoved() { const beforeMode = getDefaultFilteringMode(); const modified = syncWithBrowserPermissions(); if (modified === false) { return false; } const afterMode = getDefaultFilteringMode(); if (beforeMode > 1 && afterMode <= 1) { updateDynamicRules(); } registerInjectables(); return true; } /******************************************************************************/ function onMessage(request, sender, callback) { // Does not require trusted origin. switch (request.what) { case 'insertCSS': { const tabId = sender?.tab?.id ?? false; const frameId = sender?.frameId ?? false; if (tabId === false || frameId === false) { return; } browser.scripting.insertCSS({ css: request.css, origin: 'USER', target: { tabId, frameIds: [frameId] }, }).catch(reason => { console.log(reason); }); return false; } default: break; } // Does require trusted origin. // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender // Firefox API does not set `sender.origin` if (sender.origin !== undefined && sender.origin !== UBOL_ORIGIN) { return; } switch (request.what) { case 'applyRulesets': { enableRulesets(request.enabledRulesets).then(() => { rulesetConfig.enabledRulesets = request.enabledRulesets; return saveRulesetConfig(); }).then(() => { registerInjectables(); callback(); broadcastMessage({ enabledRulesets: rulesetConfig.enabledRulesets }); }); return true; } case 'getOptionsPageData': { Promise.all([ getDefaultFilteringMode(), getTrustedSites(), getRulesetDetails(), dnr.getEnabledRulesets(), ]).then(results => { const [ defaultFilteringMode, trustedSites, rulesetDetails, enabledRulesets, ] = results; callback({ defaultFilteringMode, trustedSites: Array.from(trustedSites), enabledRulesets, maxNumberOfEnabledRulesets: dnr.MAX_NUMBER_OF_ENABLED_STATIC_RULESETS, rulesetDetails: Array.from(rulesetDetails.values()), autoReload: rulesetConfig.autoReload, showBlockedCount: rulesetConfig.showBlockedCount, canShowBlockedCount, firstRun, }); firstRun = false; }); return true; } case 'setAutoReload': rulesetConfig.autoReload = request.state && true || false; saveRulesetConfig().then(() => { callback(); broadcastMessage({ autoReload: rulesetConfig.autoReload }); }); return true; case 'setShowBlockedCount': rulesetConfig.showBlockedCount = request.state && true || false; if (canShowBlockedCount) { dnr.setExtensionActionOptions({ displayActionCountAsBadgeText: rulesetConfig.showBlockedCount, }); } saveRulesetConfig().then(() => { callback(); broadcastMessage({ showBlockedCount: rulesetConfig.showBlockedCount }); }); return true; case 'popupPanelData': { Promise.all([ getFilteringMode(request.hostname), hasOmnipotence(), hasGreatPowers(request.origin), getEnabledRulesetsDetails(), ]).then(results => { callback({ level: results[0], autoReload: rulesetConfig.autoReload, hasOmnipotence: results[1], hasGreatPowers: results[2], rulesetDetails: results[3], }); }); return true; } case 'getFilteringMode': { getFilteringMode(request.hostname).then(actualLevel => { callback(actualLevel); }); return true; } case 'setFilteringMode': { getFilteringMode(request.hostname).then(actualLevel => { if (request.level === actualLevel) { return actualLevel; } return setFilteringMode(request.hostname, request.level); }).then(actualLevel => { registerInjectables(); callback(actualLevel); }); return true; } case 'setDefaultFilteringMode': { getDefaultFilteringMode().then(beforeLevel => setDefaultFilteringMode(request.level).then(afterLevel => ({ beforeLevel, afterLevel }) ) ).then(({ beforeLevel, afterLevel }) => { if (beforeLevel === 1 || afterLevel === 1) { updateDynamicRules(); } if (afterLevel !== beforeLevel) { registerInjectables(); } callback(afterLevel); }); return true; } case 'setTrustedSites': setTrustedSites(request.hostnames).then(() => { registerInjectables(); return Promise.all([ getDefaultFilteringMode(), getTrustedSites(), ]); }).then(results => { callback({ defaultFilteringMode: results[0], trustedSites: Array.from(results[1]), }); }); return true; default: break; } } /******************************************************************************/ async function start() { loadRulesetConfig(); if (wakeupRun === false) { enableRulesets(rulesetConfig.enabledRulesets); } // We need to update the regex rules only when ruleset version changes. if (wakeupRun === false) { const currentVersion = getCurrentVersion(); if (currentVersion !== rulesetConfig.version) { ubolLog(`Version change: ${rulesetConfig.version} => ${currentVersion}`); updateDynamicRules().then(() => { rulesetConfig.version = currentVersion; saveRulesetConfig(); }); } } // Permissions may have been removed while the extension was disabled const permissionsChanged = onPermissionsRemoved(); // Unsure whether the browser remembers correctly registered css/scripts // after we quit the browser. For now uBOL will check unconditionally at // launch time whether content css/scripts are properly registered. if (wakeupRun === false || permissionsChanged) { registerInjectables(); const enabledRulesets = dnr.getEnabledRulesets(); ubolLog(`Enabled rulesets: ${enabledRulesets}`); dnr.getAvailableStaticRuleCount().then(count => { ubolLog(`Available static rule count: ${count}`); }); } // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/declarativeNetRequest // Firefox API does not support `dnr.setExtensionActionOptions` if (wakeupRun === false && canShowBlockedCount) { dnr.setExtensionActionOptions({ displayActionCountAsBadgeText: rulesetConfig.showBlockedCount, }); } runtime.onMessage.addListener(onMessage); browser.permissions.onRemoved.addListener( () => { onPermissionsRemoved(); } ); if (firstRun) { const disableFirstRunPage = adminRead('disableFirstRunPage'); if (disableFirstRunPage !== true) { runtime.openOptionsPage(); } } } try { start(); } catch (reason) { console.trace(reason); }