From abf2eca3bba09e9999babed5b6ade0d43f6e4b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Luba=C5=84ski?= Date: Wed, 8 Jan 2025 14:38:00 +0100 Subject: [PATCH] feat(config): add remote configuration (#2152) --- src/background/autoconsent.js | 21 ++- src/background/config.js | 107 ++++++++++++++ src/background/devtools.js | 17 +++ src/background/index.js | 1 + src/pages/settings/components/devtools.js | 161 ++++++++++++++++++---- src/store/config.js | 65 +++++++++ src/utils/api.js | 5 + src/utils/engines.js | 10 +- 8 files changed, 347 insertions(+), 40 deletions(-) create mode 100644 src/background/config.js create mode 100644 src/store/config.js diff --git a/src/background/autoconsent.js b/src/background/autoconsent.js index d32c0a5c4..7f119b2d9 100644 --- a/src/background/autoconsent.js +++ b/src/background/autoconsent.js @@ -16,15 +16,24 @@ import { parse } from 'tldts-experimental'; import { store } from 'hybrids'; import Options, { isPaused } from '/store/options.js'; +import Config, { ACTION_DISABLE_AUTOCONSENT } from '/store/config.js'; async function initialize(msg, tab, frameId) { - const options = await store.resolve(Options); + const [options, config] = await Promise.all([ + store.resolve(Options), + store.resolve(Config), + ]); + + if (options.terms && options.blockAnnoyances) { + const domain = tab.url ? parse(tab.url).hostname.replace(/^www\./, '') : ''; + + if ( + isPaused(options, domain) || + config.hasAction(domain, ACTION_DISABLE_AUTOCONSENT) + ) { + return; + } - if ( - options.terms && - options.blockAnnoyances && - !isPaused(options, tab.url ? parse(tab.url).hostname : '') - ) { try { chrome.tabs.sendMessage( tab.id, diff --git a/src/background/config.js b/src/background/config.js new file mode 100644 index 000000000..318cf876a --- /dev/null +++ b/src/background/config.js @@ -0,0 +1,107 @@ +/** + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2017-present Ghostery GmbH. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { store } from 'hybrids'; + +import Config from '/store/config.js'; +import { CDN_URL } from '/utils/api.js'; + +const CONFIG_URL = CDN_URL + 'configs/v1.json'; + +function filter(item) { + if (item.filter) { + const { platform } = item.filter; + let check = true; + + // Browser check + if (check && Array.isArray(platform)) { + check = platform.includes(__PLATFORM__); + } + + return check; + } + + return true; +} + +const HALF_HOUR_IN_MS = 1000 * 60 * 30; + +export default async function syncConfig() { + const config = await store.resolve(Config); + + if (config.updatedAt > Date.now() - HALF_HOUR_IN_MS) { + return; + } + + // TODO: implement fetching remote config from the server + // This is a mock of the fetched config + try { + const fetchedConfig = await fetch(CONFIG_URL).then((res) => { + if (!res.ok) throw new Error('Failed to fetch the remote config'); + return res.json(); + }); + + // -- domains -- + + const domains = { ...config.domains }; + + // Clear out domains removed from the config + for (const name of Object.keys(domains)) { + if (fetchedConfig.domains[name] === undefined) { + domains[name] = null; + } + } + + // Update the config with new values + for (const [name, item] of Object.entries(fetchedConfig.domains)) { + domains[name] = filter(item) ? item : null; + } + + // -- flags -- + + const flags = { ...config.flags }; + + // Clear out flags removed from the config + for (const name of Object.keys(flags)) { + if (fetchedConfig.flags[name] === undefined) { + flags[name] = null; + } + } + + // Update the config with the new values + for (const [name, items] of Object.entries(fetchedConfig.flags)) { + const item = items.find((item) => filter(item)); + if (!item) { + flags[name] = null; + continue; + } + // Generate local percentage only once for each flag + const percentage = + flags[name]?.percentage || Math.floor(Math.random() * 100) + 1; + + flags[name] = { + percentage, + enabled: percentage <= item.percentage, + }; + } + + // Update the config + store.set(Config, { + updatedAt: Date.now(), + domains, + flags, + }); + } catch (e) { + console.error('[config] Failed to sync remote config:', e); + } +} + +syncConfig(); diff --git a/src/background/devtools.js b/src/background/devtools.js index bad5c8435..0bf75d9ca 100644 --- a/src/background/devtools.js +++ b/src/background/devtools.js @@ -13,9 +13,12 @@ import { store } from 'hybrids'; import DailyStats from '/store/daily-stats'; import Options from '/store/options.js'; +import Config from '/store/config.js'; import { deleteDatabases } from '/utils/indexeddb.js'; +import syncConfig from './config.js'; + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { switch (msg.action) { case 'clearStorage': @@ -33,6 +36,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { try { store.clear(Options); store.clear(DailyStats); + store.clear(Config); } catch (e) { console.error('[devtools] Error clearing store cache:', e); } @@ -45,6 +49,19 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } })(); + return true; + case 'syncConfig': + (async () => { + try { + await store.set(Config, { updatedAt: 0 }); + await syncConfig(); + + sendResponse('Config synced'); + } catch (e) { + sendResponse(`[devtools] Error syncing config: ${e}`); + } + })(); + return true; } diff --git a/src/background/index.js b/src/background/index.js index 5235b6b04..b9529cf73 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -10,6 +10,7 @@ */ import './onboarding.js'; +import './config.js'; import './autoconsent.js'; import './adblocker.js'; diff --git a/src/pages/settings/components/devtools.js b/src/pages/settings/components/devtools.js index 49c1efb92..e9fe992d4 100644 --- a/src/pages/settings/components/devtools.js +++ b/src/pages/settings/components/devtools.js @@ -12,6 +12,10 @@ import { html, store, dispatch } from 'hybrids'; import Options from '/store/options.js'; +import Config, { + ACTION_ASSIST, + ACTION_DISABLE_AUTOCONSENT, +} from '/store/config.js'; const VERSION = chrome.runtime.getManifest().version; @@ -42,6 +46,39 @@ function clearStorage(host, event) { asyncAction(event, chrome.runtime.sendMessage({ action: 'clearStorage' })); } +async function syncConfig(host, event) { + asyncAction(event, chrome.runtime.sendMessage({ action: 'syncConfig' })); +} + +async function testConfigDomain(host) { + const domain = window.prompt('Enter domain to test:', 'example.com'); + if (!domain) return; + + const actions = window.prompt( + 'Enter actions to test:', + `${ACTION_ASSIST}, ${ACTION_DISABLE_AUTOCONSENT}`, + ); + + if (!actions) return; + + await store.set(host.config, { + domains: { + [domain]: { actions: actions.split(',').map((a) => a.trim()) }, + }, + }); +} + +async function testConfigFlag(host) { + const flag = window.prompt('Enter flag to test:'); + if (!flag) return; + + await store.set(host.config, { + flags: { + [flag]: { enabled: true }, + }, + }); +} + function updateFilters(host) { if (host.updatedAt) { store.set(host.options, { filtersUpdatedAt: 0 }); @@ -57,39 +94,95 @@ function refresh(host) { } } +function formatDate(date) { + return new Date(date).toLocaleDateString(chrome.i18n.getUILanguage(), { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + export default { counter: 0, options: store(Options), + config: store(Config), updatedAt: ({ options }) => store.ready(options) && options.filtersUpdatedAt && - new Date(options.filtersUpdatedAt).toLocaleDateString( - chrome.i18n.getUILanguage(), - { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }, - ), + formatDate(options.filtersUpdatedAt), visible: false, - render: ({ visible, counter, updatedAt }) => html` + render: ({ visible, counter, updatedAt, config }) => html`