From c1130ec8438da5fcc1a0d552ae42328d3102dcc6 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Tue, 5 Jan 2021 12:16:50 -0500 Subject: [PATCH] Add support for admin-managed hidden settings Related discussion: - https://github.com/uBlockOrigin/uBlock-issues/issues/1437#issuecomment-754127066 --- platform/chromium/managed_storage.json | 36 +++++-- platform/chromium/vapi-background.js | 3 +- src/css/codemirror.css | 3 + src/css/themes/default.css | 1 + src/js/advanced-settings.js | 32 ++++-- src/js/background.js | 1 + src/js/messaging.js | 5 +- src/js/start.js | 10 +- src/js/storage.js | 136 +++++++++++++++++-------- 9 files changed, 162 insertions(+), 65 deletions(-) diff --git a/platform/chromium/managed_storage.json b/platform/chromium/managed_storage.json index 44737cfbd..850bc717f 100644 --- a/platform/chromium/managed_storage.json +++ b/platform/chromium/managed_storage.json @@ -7,13 +7,37 @@ "description": "All entries present will overwrite local settings.", "type": "string" }, - "extraTrustedSiteDirectives": { - "title": "A list of trusted-site directives", - "description": "Trusted-site directives to always add at launch time.", - "type": "array", - "items": { - "type": "string" + "toSet": { + "title": "Settings to overwrite at launch time", + "type": "object", + "properties": { + "hiddenSettings": { + "title": "A list of [name,value] pairs to populate hidden settings", + "type": "array", + "items": { + "title": "A [name,value] pair", + "type": "array", + "items": { "type": "string" } + } + }, + "trustedSiteDirectives": { + "title": "A list of trusted-site directives", + "type": "array", + "items": { "type": "string" } + } } + }, + "toAdd": { + "title": "Settings to add at launch time", + "type": "object", + "properties": { + "trustedSiteDirectives": { + "title": "A list of trusted-site directives", + "description": "Trusted-site directives to always add at launch time.", + "type": "array", + "items": { "type": "string" } + } + }, } } } diff --git a/platform/chromium/vapi-background.js b/platform/chromium/vapi-background.js index 871de996b..2cae2ba6f 100644 --- a/platform/chromium/vapi-background.js +++ b/platform/chromium/vapi-background.js @@ -1429,9 +1429,10 @@ vAPI.adminStorage = (( ) => { bin = await webext.storage.managed.get(key); } catch(ex) { } - if ( bin instanceof Object ) { + if ( typeof key === 'string' && bin instanceof Object ) { return bin[key]; } + return bin; } }; })(); diff --git a/src/css/codemirror.css b/src/css/codemirror.css index 29f450368..e00879821 100644 --- a/src/css/codemirror.css +++ b/src/css/codemirror.css @@ -91,6 +91,9 @@ text-decoration: underline var(--sf-warning-ink); text-underline-position: under; } +.cm-s-default .cm-readonly { + color: var(--sf-readonly-ink); + } /* Rules */ .cm-s-default .cm-allowrule { diff --git a/src/css/themes/default.css b/src/css/themes/default.css index b9dbb7965..93c7b521b 100644 --- a/src/css/themes/default.css +++ b/src/css/themes/default.css @@ -192,6 +192,7 @@ --sf-error-surface: #ff000016; --sf-keyword-ink: var(--purple-60); --sf-notice-ink: var(--light-gray-60); + --sf-readonly-ink: var(--light-gray-80); --sf-tag-ink: #117700; --sf-value-ink: var(--orange-80); --sf-variable-ink: var(--default-ink); diff --git a/src/js/advanced-settings.js b/src/js/advanced-settings.js index 57f53763c..a7cd8b9a1 100644 --- a/src/js/advanced-settings.js +++ b/src/js/advanced-settings.js @@ -31,6 +31,7 @@ /******************************************************************************/ let defaultSettings = new Map(); +let adminSettings = new Map(); let beforeHash = ''; /******************************************************************************/ @@ -45,18 +46,22 @@ CodeMirror.defineMode('raw-settings', function() { const match = stream.match(/\S+/); if ( match !== null && defaultSettings.has(match[0]) ) { lastSetting = match[0]; - return 'keyword'; + return adminSettings.has(match[0]) + ? 'readonly keyword' + : 'keyword'; } stream.skipToEnd(); return 'line-cm-error'; } stream.eatSpace(); const match = stream.match(/.*$/); - if ( - match !== null && - match[0].trim() !== defaultSettings.get(lastSetting) - ) { - return 'line-cm-strong'; + if ( match !== null ) { + if ( match[0].trim() !== defaultSettings.get(lastSetting) ) { + return 'line-cm-strong'; + } + if ( adminSettings.has(lastSetting) ) { + return 'readonly'; + } } stream.skipToEnd(); return null; @@ -146,21 +151,34 @@ const renderAdvancedSettings = async function(first) { what: 'readHiddenSettings', }); defaultSettings = new Map(arrayFromObject(details.default)); + adminSettings = new Map(arrayFromObject(details.admin)); beforeHash = hashFromAdvancedSettings(details.current); const pretty = []; + const roLines = []; const entries = arrayFromObject(details.current); let max = 0; for ( const [ k ] of entries ) { if ( k.length > max ) { max = k.length; } } - for ( const [ k, v ] of entries ) { + for ( let i = 0; i < entries.length; i++ ) { + const [ k, v ] = entries[i]; pretty.push(' '.repeat(max - k.length) + `${k} ${v}`); + if ( adminSettings.has(k) ) { + roLines.push(i); + } } pretty.push(''); cmEditor.setValue(pretty.join('\n')); if ( first ) { cmEditor.clearHistory(); } + for ( const line of roLines ) { + cmEditor.markText( + { line, ch: 0 }, + { line: line + 1, ch: 0 }, + { readOnly: true } + ); + } advancedSettingsChanged(); cmEditor.focus(); }; diff --git a/src/js/background.js b/src/js/background.js index 1c9cd5205..f23ec0c4c 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -108,6 +108,7 @@ const µBlock = (( ) => { // jshint ignore:line }, hiddenSettingsDefault: hiddenSettingsDefault, + hiddenSettingsAdmin: {}, hiddenSettings: Object.assign({}, hiddenSettingsDefault), // Features detection. diff --git a/src/js/messaging.js b/src/js/messaging.js index 0b778394c..693fa56fa 100644 --- a/src/js/messaging.js +++ b/src/js/messaging.js @@ -1258,8 +1258,9 @@ const onMessage = function(request, sender, callback) { case 'readHiddenSettings': response = { - current: µb.hiddenSettings, - default: µb.hiddenSettingsDefault, + 'default': µb.hiddenSettingsDefault, + 'admin': µb.hiddenSettingsAdmin, + 'current': µb.hiddenSettings, }; break; diff --git a/src/js/start.js b/src/js/start.js index d814750ba..240f9bec9 100644 --- a/src/js/start.js +++ b/src/js/start.js @@ -143,10 +143,10 @@ const onNetWhitelistReady = function(netWhitelistRaw, adminExtra) { } // Append admin-controlled trusted-site directives if ( - Array.isArray(adminExtra.trustedSites) && - adminExtra.trustedSites.length !== 0 + adminExtra instanceof Object && + Array.isArray(adminExtra.trustedSiteDirectives) ) { - for ( const directive of adminExtra.trustedSites ) { + for ( const directive of adminExtra.trustedSiteDirectives ) { µb.netWhitelistDefault.push(directive); netWhitelistRaw.push(directive); } @@ -296,9 +296,7 @@ try { ); log.info(`Backend storage for cache will be ${cacheBackend}`); - const adminExtra = {}; - adminExtra.trustedSites = - await vAPI.adminStorage.get('extraTrustedSiteDirectives') || []; + const adminExtra = await vAPI.adminStorage.get('toAdd'); log.info(`Extra admin settings ready ${Date.now()-vAPI.T0} ms after launch`); // https://github.com/uBlockOrigin/uBlock-issues/issues/1365 diff --git a/src/js/storage.js b/src/js/storage.js index f22a72966..02a03c371 100644 --- a/src/js/storage.js +++ b/src/js/storage.js @@ -89,29 +89,51 @@ /******************************************************************************/ -µBlock.loadHiddenSettings = async function() { - const bin = await vAPI.storage.get('hiddenSettings'); - if ( bin instanceof Object === false ) { return; } +// Admin hidden settings have precedence over user hidden settings. - const hs = bin.hiddenSettings; - if ( hs instanceof Object ) { - const hsDefault = this.hiddenSettingsDefault; - for ( const key in hsDefault ) { - if ( - hsDefault.hasOwnProperty(key) && - hs.hasOwnProperty(key) && - typeof hs[key] === typeof hsDefault[key] - ) { - this.hiddenSettings[key] = hs[key]; - } - } - if ( typeof this.hiddenSettings.suspendTabsUntilReady === 'boolean' ) { - this.hiddenSettings.suspendTabsUntilReady = - this.hiddenSettings.suspendTabsUntilReady - ? 'yes' - : 'unset'; +µBlock.loadHiddenSettings = async function() { + const hsDefault = this.hiddenSettingsDefault; + const hsAdmin = this.hiddenSettingsAdmin; + const hsUser = this.hiddenSettings; + + const results = await Promise.all([ + vAPI.adminStorage.get('toSet'), + vAPI.storage.get('hiddenSettings'), + ]); + + if ( + results[0] instanceof Object && + Array.isArray(results[0].hiddenSettings) + ) { + for ( const entry of results[0].hiddenSettings ) { + if ( entry.length < 1 ) { continue; } + const name = entry[0]; + if ( hsDefault.hasOwnProperty(name) === false ) { continue; } + const value = entry.length < 2 + ? hsDefault[name] + : this.hiddenSettingValueFromString(name, entry[1]); + if ( value === undefined ) { continue; } + hsDefault[name] = hsAdmin[name] = hsUser[name] = value; } } + + const hs = results[1] instanceof Object && results[1].hiddenSettings || {}; + if ( Object.keys(hsAdmin).length === 0 && Object.keys(hs).length === 0 ) { + return; + } + + for ( const key in hsDefault ) { + if ( hsDefault.hasOwnProperty(key) === false ) { continue; } + if ( hsAdmin.hasOwnProperty(name) ) { continue; } + if ( typeof hs[key] !== typeof hsDefault[key] ) { continue; } + this.hiddenSettings[key] = hs[key]; + } + if ( typeof this.hiddenSettings.suspendTabsUntilReady === 'boolean' ) { + this.hiddenSettings.suspendTabsUntilReady = + this.hiddenSettings.suspendTabsUntilReady + ? 'yes' + : 'unset'; + } this.fireDOMEvent('hiddenSettingsChanged'); }; @@ -162,31 +184,47 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { if ( matches === null || matches.length !== 3 ) { continue; } const name = matches[1]; if ( out.hasOwnProperty(name) === false ) { continue; } - const value = matches[2]; - switch ( typeof out[name] ) { - case 'boolean': - if ( value === 'true' ) { - out[name] = true; - } else if ( value === 'false' ) { - out[name] = false; - } - break; - case 'string': - out[name] = value.trim(); - break; - case 'number': - out[name] = parseInt(value, 10); - if ( isNaN(out[name]) ) { - out[name] = this.hiddenSettingsDefault[name]; - } - break; - default: - break; + if ( this.hiddenSettingsAdmin.hasOwnProperty(name) ) { continue; } + const value = this.hiddenSettingValueFromString(name, matches[2]); + if ( value !== undefined ) { + out[name] = value; } } return out; }; +µBlock.hiddenSettingValueFromString = function(name, value) { + if ( typeof name !== 'string' || typeof value !== 'string' ) { return; } + const hsDefault = this.hiddenSettingsDefault; + if ( hsDefault.hasOwnProperty(name) === false ) { return; } + let r; + switch ( typeof hsDefault[name] ) { + case 'boolean': + if ( value === 'true' ) { + r = true; + } else if ( value === 'false' ) { + r = false; + } + break; + case 'string': + r = value.trim(); + break; + case 'number': + if ( value.startsWith('0b') ) { + r = parseInt(value.slice(2), 2); + } else if ( value.startsWith('0x') ) { + r = parseInt(value.slice(2), 16); + } else { + r = parseInt(value, 10); + } + if ( isNaN(r) ) { r = undefined; } + break; + default: + break; + } + return r; +}; + µBlock.stringFromHiddenSettings = function() { const out = []; for ( const key of Object.keys(this.hiddenSettings).sort() ) { @@ -1222,9 +1260,17 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { // values are left to the user's choice. µBlock.restoreAdminSettings = async function() { + let toSet = {}; let data; try { - const json = await vAPI.adminStorage.get('adminSettings'); + const store = await vAPI.adminStorage.get([ + 'adminSettings', + 'toSet', + ]) || {}; + if ( store.toSet instanceof Object ) { + toSet = store.toSet; + } + const json = store.adminSettings; if ( typeof json === 'string' && json !== '' ) { data = JSON.parse(json); } else if ( json instanceof Object ) { @@ -1234,7 +1280,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { console.error(ex); } - if ( data instanceof Object === false ) { return; } + if ( data instanceof Object === false ) { data = {}; } const bin = {}; let binNotEmpty = false; @@ -1269,7 +1315,11 @@ self.addEventListener('hiddenSettingsChanged', ( ) => { binNotEmpty = true; } - if ( Array.isArray(data.whitelist) ) { + if ( Array.isArray(toSet.trustedSiteDirectives) ) { + µBlock.netWhitelistDefault = toSet.trustedSiteDirectives.slice(); + bin.netWhitelist = toSet.trustedSiteDirectives.slice(); + binNotEmpty = true; + } else if ( Array.isArray(data.whitelist) ) { bin.netWhitelist = data.whitelist; binNotEmpty = true; } else if ( typeof data.netWhitelist === 'string' ) {