From 0e3071dd500f13f689588a9bdf7cbc0e00305ef8 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Fri, 8 Jan 2021 09:18:26 -0500 Subject: [PATCH] Add `filterLists` property to managed storage The entry `toOverwrite.filterLists` is an array of string, where each string is a token identifying a stock filter list, or a URL for an external filter list. This new entry is to make it easier for an administrator to centrally configure uBO with a custom set of filter lists. --- platform/chromium/managed_storage.json | 6 ++ src/js/messaging.js | 6 +- src/js/start.js | 6 ++ src/js/storage.js | 79 +++++++++++++++----------- 4 files changed, 64 insertions(+), 33 deletions(-) diff --git a/platform/chromium/managed_storage.json b/platform/chromium/managed_storage.json index cca1122c8..fb58b2c61 100644 --- a/platform/chromium/managed_storage.json +++ b/platform/chromium/managed_storage.json @@ -41,6 +41,12 @@ "title": "Settings to overwrite at launch time", "type": "object", "properties": { + "filterLists": { + "title": "A collection of list identifiers and/or list URLs", + "description": "The set of filter lists to use at launch time.", + "type": "array", + "items": { "type": "string" } + }, "trustedSiteDirectives": { "title": "A list of trusted-site directives", "type": "array", diff --git a/src/js/messaging.js b/src/js/messaging.js index e107ace75..a76ca5d82 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -942,6 +942,11 @@ const restoreUserData = async function(request) { userData.hostnameSwitchesString += '\nno-csp-reports: * true'; } + // List of external lists is meant to be an array. + if ( typeof userData.externalLists === 'string' ) { + userData.externalLists = userData.externalLists.trim().split(/[\n\r]+/); + } + // https://github.com/chrisaljoudi/uBlock/issues/1102 // Ensure all currently cached assets are flushed from storage AND memory. µb.assets.rmrf(); @@ -1041,7 +1046,6 @@ const getLists = async function(callback) { cache: null, cosmeticFilterCount: µb.cosmeticFilteringEngine.getFilterCount(), current: µb.availableFilterLists, - externalLists: µb.userSettings.externalLists, ignoreGenericCosmeticFilters: µb.userSettings.ignoreGenericCosmeticFilters, isUpdating: µb.assets.isUpdating(), netFilterCount: µb.staticNetFilteringEngine.getFilterCount(), diff --git a/src/js/start.js b/src/js/start.js index 240f9bec9..3ab9ca221 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -162,6 +162,12 @@ const onNetWhitelistReady = function(netWhitelistRaw, adminExtra) { const onUserSettingsReady = function(fetched) { const userSettings = µb.userSettings; + // List of external lists is meant to be an array + if ( typeof fetched.externalLists === 'string' ) { + fetched.externalLists = + fetched.externalLists.trim().split(/[\n\r]+/); + } + fromFetch(userSettings, fetched); if ( µb.privacySettingsSupported ) { diff --git a/src/js/storage.js b/src/js/storage.js index 65790413c..7852c8f2a 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -335,7 +335,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { µBlock.applyFilterListSelection = function(details) { let selectedListKeySet = new Set(this.selectedFilterLists); - let externalLists = this.userSettings.externalLists; + let externalLists = this.userSettings.externalLists.slice(); // Filter lists to select if ( Array.isArray(details.toSelect) ) { @@ -350,19 +350,13 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { // Imported filter lists to remove if ( Array.isArray(details.toRemove) ) { - const removeURLFromHaystack = (haystack, needle) => { - return haystack.replace( - new RegExp( - '(^|\\n)' + - this.escapeRegex(needle) + - '(\\n|$)', 'g'), - '\n' - ).trim(); - }; for ( let i = 0, n = details.toRemove.length; i < n; i++ ) { const assetKey = details.toRemove[i]; selectedListKeySet.delete(assetKey); - externalLists = removeURLFromHaystack(externalLists, assetKey); + const pos = externalLists.indexOf(assetKey); + if ( pos !== -1 ) { + externalLists.splice(pos, 1); + } this.removeFilterList(assetKey); } } @@ -404,11 +398,11 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { } selectedListKeySet.add(assetKey); } - externalLists = Array.from(importedSet).sort().join('\n'); + externalLists = Array.from(importedSet).sort(); } const result = Array.from(selectedListKeySet); - if ( externalLists !== this.userSettings.externalLists ) { + if ( externalLists.join() !== this.userSettings.externalLists.join() ) { this.userSettings.externalLists = externalLists; vAPI.storage.set({ externalLists }); } @@ -418,16 +412,17 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { /******************************************************************************/ µBlock.listKeysFromCustomFilterLists = function(raw) { + const urls = typeof raw === 'string' + ? raw.trim().split(/[\n\r]+/) + : raw; const out = new Set(); const reIgnore = /^[!#]/; const reValid = /^[a-z-]+:\/\/\S+/; - const lineIter = new this.LineIterator(raw); - while ( lineIter.eot() === false ) { - const location = lineIter.next().trim(); - if ( reIgnore.test(location) || !reValid.test(location) ) { continue; } + for ( const url of urls ) { + if ( reIgnore.test(url) || !reValid.test(url) ) { continue; } // Ignore really bad lists. - if ( this.badLists.get(location) === true ) { continue; } - out.add(location); + if ( this.badLists.get(url) === true ) { continue; } + out.add(url); } return Array.from(out); }; @@ -603,8 +598,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { newAvailableLists[listURL] = newEntry; this.assets.registerAssetSource(listURL, newEntry); importedListKeys.push(listURL); - this.userSettings.externalLists += '\n' + listURL; - this.userSettings.externalLists = this.userSettings.externalLists.trim(); + this.userSettings.externalLists.push(listURL.trim()); vAPI.storage.set({ externalLists: this.userSettings.externalLists }); this.saveSelectedFilterLists([ listURL ], true); }; @@ -1322,26 +1316,47 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { } if ( typeof data.userSettings === 'object' ) { - for ( const name in this.userSettings ) { - if ( this.userSettings.hasOwnProperty(name) === false ) { - continue; - } - if ( data.userSettings.hasOwnProperty(name) === false ) { - continue; - } - bin[name] = data.userSettings[name]; + const µbus = this.userSettings; + const adminus = data.userSettings; + // List of external lists is meant to be an array. + if ( typeof adminus.externalLists === 'string' ) { + adminus.externalLists = + adminus.externalLists.trim().split(/[\n\r]+/); + } + for ( const name in µbus ) { + if ( µbus.hasOwnProperty(name) === false ) { continue; } + if ( adminus.hasOwnProperty(name) === false ) { continue; } + bin[name] = adminus[name]; binNotEmpty = true; } } // 'selectedFilterLists' is an array of filter list tokens. Each token - // is a reference to an asset in 'assets.json'. - if ( Array.isArray(data.selectedFilterLists) ) { + // is a reference to an asset in 'assets.json', or a URL for lists not + // present in 'assets.json'. + if ( + Array.isArray(toOverwrite.filterLists) && + toOverwrite.filterLists.length !== 0 + ) { + const externalLists = []; + for ( const list of toOverwrite.filterLists ) { + if ( /^[a-z-]+:\/\//.test(list) === false ) { continue; } + externalLists.push(list); + } + if ( externalLists.length !== 0 ) { + bin.externalLists = externalLists; + } + bin.selectedFilterLists = toOverwrite.filterLists; + binNotEmpty = true; + } else if ( Array.isArray(data.selectedFilterLists) ) { bin.selectedFilterLists = data.selectedFilterLists; binNotEmpty = true; } - if ( Array.isArray(toOverwrite.trustedSiteDirectives) ) { + if ( + Array.isArray(toOverwrite.trustedSiteDirectives) && + toOverwrite.trustedSiteDirectives.length !== 0 + ) { µBlock.netWhitelistDefault = toOverwrite.trustedSiteDirectives.slice(); bin.netWhitelist = toOverwrite.trustedSiteDirectives.slice(); binNotEmpty = true;