Skip to content

Commit

Permalink
feat(config): add remote configuration (#2152)
Browse files Browse the repository at this point in the history
  • Loading branch information
smalluban authored Jan 8, 2025
1 parent 8e53ceb commit abf2eca
Show file tree
Hide file tree
Showing 8 changed files with 347 additions and 40 deletions.
21 changes: 15 additions & 6 deletions src/background/autoconsent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
107 changes: 107 additions & 0 deletions src/background/config.js
Original file line number Diff line number Diff line change
@@ -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();
17 changes: 17 additions & 0 deletions src/background/devtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand All @@ -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);
}
Expand All @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions src/background/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import './onboarding.js';
import './config.js';

import './autoconsent.js';
import './adblocker.js';
Expand Down
161 changes: 134 additions & 27 deletions src/pages/settings/components/devtools.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 });
Expand All @@ -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`
<template layout="column gap:3">
${
(visible || counter > 5) &&
html`
<section layout="column gap:3" translate="no">
<ui-text type="headline-m">Developer tools</ui-text>
<div layout="column gap">
<ui-text type="headline-s">Storage actions</ui-text>
<div layout="row gap items:start">
<ui-button onclick="${clearStorage}" layout="shrink:0">
<button>Clear local storage</button>
</ui-button>
${store.ready(config) &&
html`
<div layout="column gap" translate="no">
<ui-toggle
value="${config.enabled}"
onchange="${html.set(config, 'enabled')}"
>
<div layout="column">
<ui-text type="headline-s">Remote Configuration</ui-text>
<ui-text type="body-xs" color="gray-400">
Updated at: ${formatDate(config.updatedAt)}
</ui-text>
</div>
</ui-toggle>
<div>
<ui-text type="label-m">Domains</ui-text>
<div layout="row:wrap gap">
${Object.entries(config.domains)
.filter(([, d]) => d.actions.length)
.map(
([name, d]) =>
html`<ui-text color="gray-600">
${name} (${d.actions.join(', ')})
</ui-text>`,
) || 'none'}
</div>
</div>
<div>
<ui-text type="label-m">Flags</ui-text>
<ui-text color="gray-600">
${Object.entries(config.flags)
.filter(([, f]) => f.enabled)
.map(([name]) => name)
.join(' ') || 'none'}
</ui-text>
</div>
<div layout="row gap">
<ui-button
layout="shrink:0 self:start"
onclick="${testConfigDomain}"
>
<button>Test domain</button>
</ui-button>
<ui-button
layout="shrink:0 self:start"
onclick="${testConfigFlag}"
>
<button>Test flag</button>
</ui-button>
<ui-button
onclick="${syncConfig}"
layout="shrink:0 self:start"
>
<button>
<ui-icon name="refresh" layout="size:2"></ui-icon>
Force sync
</button>
</ui-button>
</div>
</div>
</div>
<ui-line></ui-line>
<ui-line></ui-line>
`}
${(__PLATFORM__ === 'chromium' || __PLATFORM__ === 'safari') &&
html`
<div layout="column gap items:start" translate="no">
Expand Down Expand Up @@ -119,19 +212,33 @@ export default {
</div>
<ui-line></ui-line>
`}
<div layout="column gap">
<ui-text type="headline-s">Local Storage</ui-text>
<div layout="row gap items:start">
<ui-button onclick="${clearStorage}" layout="shrink:0">
<button>
<ui-icon name="trash" layout="size:2"></ui-icon>
Clear storage
</button>
</ui-button>
</div>
</div>
</section>
`
}
<div layout="column gap center">
<div layout="row center gap:2">
<ui-text
type="label-s"
color="gray-300"
onclick="${refresh}"
translate="no"
>
v${VERSION}
</ui-text>
<div onclick="${refresh}">
<ui-text
type="label-s"
color="gray-300"
translate="no"
style="user-select: none;"
>
v${VERSION}
</ui-text>
</div>
</div>
<ui-action>
<ui-text type="label-xs" color="gray-300" onclick="${updateFilters}">
Expand Down
Loading

0 comments on commit abf2eca

Please sign in to comment.