/******************************************************************************* 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, windows, } from './ext.js'; import { defaultRulesetsFromLanguage, enableRulesets, getEnabledRulesetsDetails, getRulesetDetails, updateDynamicRules, } from './ruleset-manager.js'; import { MODE_BASIC, MODE_OPTIMAL, getDefaultFilteringMode, getFilteringMode, getTrustedSites, setDefaultFilteringMode, setFilteringMode, setTrustedSites, syncWithBrowserPermissions, } from './mode-manager.js'; import { getMatchedRules, isSideloaded, ubolLog, } from './debug.js'; import { broadcastMessage } from './utils.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 = await 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 = await 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 = await 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 = await getDefaultFilteringMode(); const modified = await syncWithBrowserPermissions(); if ( modified === false ) { return false; } const afterMode = await getDefaultFilteringMode(); if ( beforeMode > MODE_BASIC && afterMode <= MODE_BASIC ) { updateDynamicRules(); } registerInjectables(); return true; } /******************************************************************************/ async function gotoURL(url, type) { const pageURL = new URL(url, runtime.getURL('/')); const tabs = await browser.tabs.query({ url: pageURL.href, windowType: type !== 'popup' ? 'normal' : 'popup' }); if ( Array.isArray(tabs) && tabs.length !== 0 ) { const { windowId, id } = tabs[0]; return Promise.all([ browser.windows.update(windowId, { focused: true }), browser.tabs.update(id, { active: true }), ]); } if ( type === 'popup' ) { return windows.create({ type: 'popup', url: pageURL.href, }); } return browser.tabs.create({ active: true, url: pageURL.href, }); } /******************************************************************************/ 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], isSideloaded, }); }); return true; } case 'getFilteringMode': { getFilteringMode(request.hostname).then(actualLevel => { callback(actualLevel); }); return true; } case 'gotoURL': gotoURL(request.url, request.type); break; 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 'getDefaultFilteringMode': { getDefaultFilteringMode().then(level => { callback(level); }); 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; case 'getMatchedRules': getMatchedRules(request.tabId).then(entries => { callback(entries); }); return true; case 'showMatchedRules': windows.create({ type: 'popup', url: `/matched-rules.html?tab=${request.tabId}`, }); break; default: break; } } /******************************************************************************/ async function start() { await loadRulesetConfig(); if ( wakeupRun === false ) { await 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 = await 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 = await 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 enableOptimal = await hasOmnipotence(); if ( enableOptimal ) { const afterLevel = await setDefaultFilteringMode(MODE_OPTIMAL); if ( afterLevel === MODE_OPTIMAL ) { updateDynamicRules(); registerInjectables(); } } const disableFirstRunPage = await adminRead('disableFirstRunPage'); if ( disableFirstRunPage !== true ) { runtime.openOptionsPage(); } else { firstRun = false; } } } // https://github.com/uBlockOrigin/uBOL-home/issues/199 // Force a restart of the extension once when an "internal error" occurs start().then(( ) => { localWrite({ goodStart: true }); }).catch(reason => { console.trace(reason); localRead('goodStart').then((bin = {}) => { if ( bin.goodStart === false ) { return; } localWrite({ goodStart: false }).then(( ) => { runtime.reload(); }); }); });