mirror of
https://github.com/gorhill/uBlock.git
synced 2026-03-11 09:04:36 +00:00
[mv3] Add ability to backup/restore settings
Related issue: https://github.com/uBlockOrigin/uBOL-home/issues/482
This commit is contained in:
parent
f89de42364
commit
2581004e8f
9 changed files with 278 additions and 17 deletions
|
|
@ -263,6 +263,18 @@
|
|||
"message": "Enables access to features suitable for technical users.",
|
||||
"description": "Short description for a checkbox in the options page"
|
||||
},
|
||||
"settingsBackupRestoreLabel": {
|
||||
"message": "Backup / Restore",
|
||||
"description": "The header text for the backup/restore section"
|
||||
},
|
||||
"settingsBackupRestoreSummary": {
|
||||
"message": "Backup your custom settings to a file, or restore your custom settings from a file.",
|
||||
"description": "A summary description of the backup/restore section."
|
||||
},
|
||||
"settingsBackupRestoreLegend": {
|
||||
"message": "Restoring will overwrite all your current custom settings.",
|
||||
"description": "Important information about the backup/restore section."
|
||||
},
|
||||
"findListsPlaceholder": {
|
||||
"message": "Find lists",
|
||||
"description": "Placeholder for the input field used to find lists"
|
||||
|
|
@ -363,6 +375,14 @@
|
|||
"message": "Export…",
|
||||
"description": "Text for buttons used to export content"
|
||||
},
|
||||
"backupButton": {
|
||||
"message": "Backup…",
|
||||
"description": "Text for buttons used to backup content"
|
||||
},
|
||||
"restoreButton": {
|
||||
"message": "Restore…",
|
||||
"description": "Text for buttons used to restore content"
|
||||
},
|
||||
"dnrRulesWarning": {
|
||||
"message": "Do not add content from untrusted sources",
|
||||
"description": "Short description of the DNR rules editor pane"
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@
|
|||
</p>
|
||||
<p><label id="showBlockedCount" data-i18n="showBlockedCountLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div>
|
||||
<h3 data-i18n="defaultFilteringModeSectionLabel"></h3>
|
||||
<p data-i18n="defaultFilteringModeDescription"></p>
|
||||
|
|
@ -98,6 +98,15 @@
|
|||
<p data-platform-exclude="safari"><label id="strictBlockMode" data-i18n="enableStrictBlockLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label><legend data-i18n="enableStrictBlockLegend"></legend>
|
||||
<p id="developerMode"><label data-i18n="developerModeLabel"><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span>_</label><legend data-i18n="developerModeLegend"></legend>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<h3 data-i18n="settingsBackupRestoreLabel">_</h3>
|
||||
<p data-i18n="settingsBackupRestoreSummary">_</p>
|
||||
<p>
|
||||
<button class="iconified dontshrink" type="button"><span class="fa-icon">download-alt</span><span data-i18n="backupButton">_</span><span class="hover"></span></button> 
|
||||
<button class="iconified dontshrink" type="button"><span class="fa-icon">upload-alt</span><span data-i18n="restoreButton">_</span><span class="hover"></span></button><input type="file" accept="application/json">
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
<!-- -------- -->
|
||||
<section data-pane="rulesets">
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
import {
|
||||
MODE_BASIC,
|
||||
MODE_OPTIMAL,
|
||||
defaultFilteringModes,
|
||||
getDefaultFilteringMode,
|
||||
getFilteringMode,
|
||||
getFilteringModeDetails,
|
||||
|
|
@ -63,9 +64,18 @@ import {
|
|||
webextFlavor,
|
||||
} from './ext.js';
|
||||
|
||||
import {
|
||||
defaultConfig,
|
||||
loadRulesetConfig,
|
||||
process,
|
||||
rulesetConfig,
|
||||
saveRulesetConfig,
|
||||
} from './config.js';
|
||||
|
||||
import {
|
||||
enableRulesets,
|
||||
excludeFromStrictBlock,
|
||||
getDefaultRulesetsFromEnv,
|
||||
getEffectiveDynamicRules,
|
||||
getEffectiveSessionRules,
|
||||
getEffectiveUserRules,
|
||||
|
|
@ -85,13 +95,6 @@ import {
|
|||
ubolLog,
|
||||
} from './debug.js';
|
||||
|
||||
import {
|
||||
loadRulesetConfig,
|
||||
process,
|
||||
rulesetConfig,
|
||||
saveRulesetConfig,
|
||||
} from './config.js';
|
||||
|
||||
import { dnr } from './ext-compat.js';
|
||||
import { getTroubleshootingInfo } from './troubleshooting.js';
|
||||
import { registerInjectables } from './scripting-manager.js';
|
||||
|
|
@ -272,6 +275,19 @@ function onMessage(request, sender, callback) {
|
|||
return true;
|
||||
}
|
||||
|
||||
case 'getDefaultConfig':
|
||||
getDefaultRulesetsFromEnv().then(rulesets => {
|
||||
callback({
|
||||
autoReload: defaultConfig.autoReload,
|
||||
developerMode: defaultConfig.developerMode,
|
||||
showBlockedCount: defaultConfig.showBlockedCount,
|
||||
strictBlockMode: defaultConfig.strictBlockMode,
|
||||
rulesets,
|
||||
filteringModes: Object.assign(defaultFilteringModes),
|
||||
});
|
||||
});
|
||||
return true;
|
||||
|
||||
case 'getOptionsPageData':
|
||||
Promise.all([
|
||||
hasBroadHostPermissions(),
|
||||
|
|
|
|||
149
platform/mv3/extension/js/backup-restore.js
Normal file
149
platform/mv3/extension/js/backup-restore.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/*******************************************************************************
|
||||
|
||||
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 {
|
||||
localRead, localWrite,
|
||||
runtime,
|
||||
sendMessage,
|
||||
} from './ext.js';
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function backupToObject(currentConfig) {
|
||||
const out = {};
|
||||
const manifest = runtime.getManifest();
|
||||
out.version = manifest.versionName ?? manifest.version;
|
||||
const defaultConfig = await sendMessage({ what: 'getDefaultConfig' });
|
||||
if ( currentConfig.autoReload !== defaultConfig.autoReload ) {
|
||||
out.autoReload = currentConfig.autoReload;
|
||||
}
|
||||
if ( currentConfig.developerMode !== defaultConfig.developerMode ) {
|
||||
out.developerMode = currentConfig.developerMode;
|
||||
}
|
||||
if ( currentConfig.showBlockedCount !== defaultConfig.showBlockedCount ) {
|
||||
out.showBlockedCount = currentConfig.showBlockedCount;
|
||||
}
|
||||
if ( currentConfig.strictBlockMode !== defaultConfig.strictBlockMode ) {
|
||||
out.strictBlockMode = currentConfig.strictBlockMode;
|
||||
}
|
||||
const { enabledRulesets } = currentConfig;
|
||||
const customRulesets = [];
|
||||
for ( const id of enabledRulesets ) {
|
||||
if ( defaultConfig.rulesets.includes(id) ) { continue; }
|
||||
customRulesets.push(`+${id}`);
|
||||
}
|
||||
for ( const id of defaultConfig.rulesets ) {
|
||||
if ( enabledRulesets.includes(id) ) { continue; }
|
||||
customRulesets.push(`-${id}`);
|
||||
}
|
||||
if ( customRulesets.length !== 0 ) {
|
||||
out.rulesets = customRulesets;
|
||||
}
|
||||
out.filteringModes = await sendMessage({ what: 'getFilteringModeDetails' });
|
||||
const customFilters = await sendMessage({ what: 'getAllCustomFilters' });
|
||||
const filters = [];
|
||||
for ( const [ hostname, selectors ] of customFilters ) {
|
||||
for ( const selector of selectors ) {
|
||||
filters.push(`${hostname}##${selector}`);
|
||||
}
|
||||
}
|
||||
if ( filters.length !== 0 ) {
|
||||
out.cosmeticFilters = filters;
|
||||
}
|
||||
const dnrRules = await localRead('userDnrRules');
|
||||
if ( typeof dnrRules === 'string' && dnrRules.length !== 0 ) {
|
||||
out.dnrRules = dnrRules.split(/\n+/);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
export async function restoreFromObject(targetConfig) {
|
||||
const defaultConfig = await sendMessage({ what: 'getDefaultConfig' });
|
||||
|
||||
await sendMessage({
|
||||
what: 'setAutoReload',
|
||||
state: targetConfig.autoReload ?? defaultConfig.autoReload
|
||||
});
|
||||
|
||||
await sendMessage({
|
||||
what: 'setShowBlockedCount',
|
||||
state: targetConfig.showBlockedCount ?? defaultConfig.showBlockedCount
|
||||
});
|
||||
|
||||
await sendMessage({
|
||||
what: 'setDeveloperMode',
|
||||
state: targetConfig.developerMode ?? defaultConfig.developerMode
|
||||
});
|
||||
|
||||
await sendMessage({
|
||||
what: 'setStrictBlockMode',
|
||||
state: targetConfig.strictBlockMode ?? defaultConfig.strictBlockMode
|
||||
});
|
||||
|
||||
const enabledRulesets = new Set(defaultConfig.rulesets);
|
||||
for ( const entry of targetConfig.rulesets ) {
|
||||
const id = entry.slice(1);
|
||||
if ( entry.startsWith('+') ) {
|
||||
enabledRulesets.add(id);
|
||||
} else if ( entry.startsWith('-') ) {
|
||||
enabledRulesets.delete(id);
|
||||
}
|
||||
}
|
||||
await sendMessage({
|
||||
what: 'applyRulesets',
|
||||
enabledRulesets: Array.from(enabledRulesets),
|
||||
});
|
||||
|
||||
await sendMessage({
|
||||
what: 'setFilteringModeDetails',
|
||||
modes: targetConfig.filteringModes ?? defaultConfig.filteringModes,
|
||||
});
|
||||
|
||||
await sendMessage({ what: 'removeAllCustomFilters', hostname: '*' });
|
||||
const hostnameMap = new Map();
|
||||
for ( const line of targetConfig.cosmeticFilters ?? [] ) {
|
||||
const i = line.indexOf('##');
|
||||
if ( i === -1 ) { continue; }
|
||||
const hostname = line.slice(0, i);
|
||||
if ( hostname === '' ) { continue; }
|
||||
const selector = line.slice(i+2);
|
||||
if ( selector === '' ) { continue; }
|
||||
const selectors = hostnameMap.get(hostname) || [];
|
||||
if ( selectors.length === 0 ) {
|
||||
hostnameMap.set(hostname, selectors)
|
||||
}
|
||||
selectors.push(selector);
|
||||
}
|
||||
const promises = [];
|
||||
for ( const [ hostname, selectors ] of hostnameMap ) {
|
||||
promises.push(
|
||||
sendMessage({ what: 'addCustomFilters', hostname, selectors })
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
const dnrRules = targetConfig.dnrRules ?? [];
|
||||
await localWrite('userDnrRules', dnrRules.join('\n'));
|
||||
await sendMessage({ what: 'updateUserDnrRules' });
|
||||
|
||||
}
|
||||
|
|
@ -37,6 +37,8 @@ export const rulesetConfig = {
|
|||
hasBroadHostPermissions: true,
|
||||
};
|
||||
|
||||
export const defaultConfig = Object.assign({}, rulesetConfig);
|
||||
|
||||
export const process = {
|
||||
firstRun: false,
|
||||
wakeupRun: false,
|
||||
|
|
|
|||
|
|
@ -227,6 +227,14 @@ export async function addCustomFilters(hostname, toAdd) {
|
|||
/******************************************************************************/
|
||||
|
||||
export async function removeAllCustomFilters(hostname) {
|
||||
if ( hostname === '*' ) {
|
||||
const keys = await getAllCustomFilterKeys();
|
||||
if ( keys.length === 0 ) { return false; }
|
||||
for ( const key of keys ) {
|
||||
removeFromStorage(key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const key = `site.${hostname}`;
|
||||
const selectors = await readFromStorage(key) || [];
|
||||
removeFromStorage(key);
|
||||
|
|
|
|||
|
|
@ -53,6 +53,13 @@ export const MODE_BASIC = 1;
|
|||
export const MODE_OPTIMAL = 2;
|
||||
export const MODE_COMPLETE = 3;
|
||||
|
||||
export const defaultFilteringModes = {
|
||||
none: [],
|
||||
basic: [],
|
||||
optimal: [ 'all-urls' ],
|
||||
complete: [],
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const pruneDescendantHostnamesFromSet = (hostname, hnSet) => {
|
||||
|
|
@ -225,7 +232,7 @@ export async function readFilteringModeDetails(bypassCache = false) {
|
|||
}
|
||||
}
|
||||
let [
|
||||
userModes = { optimal: [ 'all-urls' ] },
|
||||
userModes = structuredClone(defaultFilteringModes),
|
||||
adminDefaultFiltering,
|
||||
adminNoFiltering,
|
||||
] = await Promise.all([
|
||||
|
|
|
|||
|
|
@ -75,12 +75,11 @@ function getRulesetDetails() {
|
|||
if ( getRulesetDetails.rulesetDetailsPromise !== undefined ) {
|
||||
return getRulesetDetails.rulesetDetailsPromise;
|
||||
}
|
||||
getRulesetDetails.rulesetDetailsPromise = fetchJSON('/rulesets/ruleset-details').then(entries => {
|
||||
const rulesMap = new Map(
|
||||
entries.map(entry => [ entry.id, entry ])
|
||||
);
|
||||
return rulesMap;
|
||||
});
|
||||
getRulesetDetails.rulesetDetailsPromise =
|
||||
fetchJSON('/rulesets/ruleset-details').then(entries => {
|
||||
const rulesMap = new Map(entries.map(entry => [ entry.id, entry ]));
|
||||
return rulesMap;
|
||||
});
|
||||
return getRulesetDetails.rulesetDetailsPromise;
|
||||
}
|
||||
|
||||
|
|
@ -410,7 +409,7 @@ async function filteringModesToDNR(modes) {
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
async function defaultRulesetsFromEnv() {
|
||||
export async function getDefaultRulesetsFromEnv() {
|
||||
const dropCountry = lang => {
|
||||
const pos = lang.indexOf('-');
|
||||
if ( pos === -1 ) { return lang; }
|
||||
|
|
@ -466,7 +465,7 @@ async function patchDefaultRulesets() {
|
|||
staticRulesetIds,
|
||||
] = await Promise.all([
|
||||
localRead('defaultRulesetIds'),
|
||||
defaultRulesetsFromEnv(),
|
||||
getDefaultRulesetsFromEnv(),
|
||||
getStaticRulesets().then(r => r.map(a => a.id)),
|
||||
]);
|
||||
const toAdd = [];
|
||||
|
|
|
|||
|
|
@ -132,6 +132,49 @@ dom.on(
|
|||
|
||||
/******************************************************************************/
|
||||
|
||||
async function backupSettings() {
|
||||
const api = await import('./backup-restore.js');
|
||||
const data = await api.backupToObject(cachedRulesetData);
|
||||
if ( data instanceof Object === false ) { return; }
|
||||
const json = JSON.stringify(data, null, 2) + '\n';
|
||||
const a = document.createElement('a');
|
||||
a.href = `data:text/plain;charset=utf-8,${encodeURIComponent(json)}`;
|
||||
dom.attr(a, 'download', 'my-ubol-settings.json');
|
||||
dom.attr(a, 'type', 'application/json');
|
||||
a.click();
|
||||
}
|
||||
|
||||
async function restoreSettings() {
|
||||
const input = qs$('section[data-pane="settings"] input[type="file"]');
|
||||
input.onchange = ev => {
|
||||
input.onchange = null;
|
||||
const file = ev.target.files[0];
|
||||
if ( file === undefined || file.name === '' ) { return; }
|
||||
const fr = new FileReader();
|
||||
fr.onload = ( ) => {
|
||||
fr.onload = null;
|
||||
if ( typeof fr.result !== 'string' ) { return; }
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(fr.result);
|
||||
} catch {
|
||||
}
|
||||
if ( data instanceof Object === false ) { return; }
|
||||
import('./backup-restore.js').then(api => {
|
||||
api.restoreFromObject(data);
|
||||
});
|
||||
};
|
||||
fr.readAsText(file);
|
||||
};
|
||||
// Reset to empty string, this will ensure a change event is properly
|
||||
// triggered if the user pick a file, even if it's the same as the last
|
||||
// one picked.
|
||||
input.value = '';
|
||||
input.click();
|
||||
}
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
dom.on('#autoReload input[type="checkbox"]', 'change', ev => {
|
||||
sendMessage({
|
||||
what: 'setAutoReload',
|
||||
|
|
@ -159,6 +202,14 @@ dom.on('#developerMode input[type="checkbox"]', 'change', ev => {
|
|||
dom.body.dataset.develop = `${state}`;
|
||||
});
|
||||
|
||||
dom.on('section[data-pane="settings"] [data-i18n="backupButton"]', 'click', ( ) => {
|
||||
backupSettings();
|
||||
});
|
||||
|
||||
dom.on('section[data-pane="settings"] [data-i18n="restoreButton"]', 'click', ( ) => {
|
||||
restoreSettings();
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
function listen() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue