Allow features to require optional permissions
Design doc:
https://docs.google.com/document/d/1OhL0Yh7SmWffXyjW_XVQOK95Fqh7gLltk1eEtnKN8Ds/edit
Fixed: twpowertools:86
Change-Id: Iccb22aac2b285307854b7a4c002e9702c24d57f2
diff --git a/package-lock.json b/package-lock.json
index 77eb6b2..7a6ad3c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
"style-loader": "^3.2.1",
"webpack": "^5.44.0",
"webpack-cli": "^4.7.2",
+ "webpack-preprocessor-loader": "^1.1.4",
"webpack-shell-plugin-next": "^2.2.2"
}
},
@@ -2187,6 +2188,15 @@
"node": ">=10.0.0"
}
},
+ "node_modules/webpack-preprocessor-loader": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/webpack-preprocessor-loader/-/webpack-preprocessor-loader-1.1.4.tgz",
+ "integrity": "sha512-Ajjj0ns6hwCb5DemvDLp3v5JYHZ0B6+K+CsD3Q/HUc8CWlCTC4QgrUXhZ6oCZG+MDuw44suw2Q3UOZ19FbqzjA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.11.5"
+ }
+ },
"node_modules/webpack-shell-plugin-next": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/webpack-shell-plugin-next/-/webpack-shell-plugin-next-2.2.2.tgz",
@@ -3848,6 +3858,12 @@
"wildcard": "^2.0.0"
}
},
+ "webpack-preprocessor-loader": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/webpack-preprocessor-loader/-/webpack-preprocessor-loader-1.1.4.tgz",
+ "integrity": "sha512-Ajjj0ns6hwCb5DemvDLp3v5JYHZ0B6+K+CsD3Q/HUc8CWlCTC4QgrUXhZ6oCZG+MDuw44suw2Q3UOZ19FbqzjA==",
+ "dev": true
+ },
"webpack-shell-plugin-next": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/webpack-shell-plugin-next/-/webpack-shell-plugin-next-2.2.2.tgz",
diff --git a/package.json b/package.json
index aab39ac..3263d41 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"style-loader": "^3.2.1",
"webpack": "^5.44.0",
"webpack-cli": "^4.7.2",
+ "webpack-preprocessor-loader": "^1.1.4",
"webpack-shell-plugin-next": "^2.2.2"
},
"private": true,
diff --git a/src/background.js b/src/background.js
index a2bb071..7dc5faf 100644
--- a/src/background.js
+++ b/src/background.js
@@ -1,5 +1,6 @@
// IMPORTANT: keep this file in sync with sw.js
-import {cleanUpOptions} from './common/optionsUtils.js';
+import {cleanUpOptPermissions} from './common/optionsPermissions.js';
+import {cleanUpOptions, disableItemsWithMissingPermissions} from './common/optionsUtils.js';
import KillSwitchMechanism from './killSwitch/index.js';
chrome.browserAction.onClicked.addListener(function() {
@@ -33,3 +34,34 @@
killSwitchMechanism.updateKillSwitchStatus();
}
});
+
+// Clean up optional permissions and check that none are missing for enabled
+// features as soon as the extension starts and when the options change.
+cleanUpOptPermissions();
+
+chrome.storage.sync.onChanged.addListener(() => {
+ cleanUpOptPermissions();
+});
+
+chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
+ if (sender.id !== chrome.runtime.id)
+ return console.warn(
+ 'An unknown sender (' + sender.id +
+ ') sent a message to the extension: ',
+ msg);
+
+ console.assert(msg.message);
+ switch (msg.message) {
+ case 'runDisableItemsWithMissingPermissions':
+ console.assert(
+ msg.options?.items && msg.options?.permissionChecksFeatures);
+ disableItemsWithMissingPermissions(
+ msg.options?.items, msg.options?.permissionChecksFeatures)
+ .then(items => sendResponse({status: 'resolved', items}))
+ .catch(error => sendResponse({status: 'rejected', error}));
+ break;
+
+ default:
+ console.warn('Unknown message "' + msg.message + '".');
+ }
+});
diff --git a/src/common/optionsPermissions.js b/src/common/optionsPermissions.js
new file mode 100644
index 0000000..9a998ad
--- /dev/null
+++ b/src/common/optionsPermissions.js
@@ -0,0 +1,178 @@
+import optionsPrototype from './optionsPrototype.json5';
+import {getOptions} from './optionsUtils.js';
+
+// #!if browser_target == 'chromium_mv3'
+const actionApi = chrome.action;
+// #!else
+const actionApi = chrome.browserAction;
+// #!endif
+
+// Required permissions, including host permissions.
+//
+// IMPORTANT: This should be kept in sync with the "permissions" key in
+// //templates/manifest.gjson.
+const requiredPermissions = new Set([
+ 'storage',
+ 'alarms',
+]);
+
+// Returns an array of optional permissions needed by |feature|.
+export function requiredOptPermissions(feature) {
+ if (!(feature in optionsPrototype)) {
+ console.error('"' + feature + '" feature doesn\'t exist.');
+ return [];
+ }
+
+ return optionsPrototype[feature]?.requiredOptPermissions ?? [];
+}
+
+// Returns a promise resolving to an array of optional permissions needed by all
+// the current enabled features.
+export function currentRequiredOptPermissions() {
+ return getOptions(null, /* requireOptionalPermissions = */ false)
+ .then(options => {
+ let permissions = [];
+
+ // For each option
+ for (const [opt, optMeta] of Object.entries(optionsPrototype))
+ // If the option is enabled
+ if (options[opt])
+ // Add its required optional permissions to the list
+ permissions.push(...(optMeta.requiredOptPermissions ?? []));
+
+ return permissions;
+ });
+}
+
+// Ensures that all the optional permissions required by |feature| are granted,
+// and requests them otherwise. It returns a promise which resolves specifying
+// whether the permissions were granted or not.
+export function ensureOptPermissions(feature) {
+ return new Promise((resolve, reject) => {
+ let permissions = requiredOptPermissions(feature);
+
+ chrome.permissions.contains({permissions}, isAlreadyGranted => {
+ if (isAlreadyGranted) return resolve(true);
+
+ chrome.permissions.request({permissions}, granted => {
+ // If there was an error, reject the promise.
+ if (granted === undefined)
+ return reject(new Error(
+ chrome.runtime.lastError.message ??
+ 'An unknown error occurred while requesting the permisisons'));
+
+ // If the permission is granted we should maybe remove the warning
+ // badge.
+ if (granted) cleanUpOptPermissions(/* removeLeftoverPerms = */ false);
+
+ return resolve(granted);
+ });
+ });
+ });
+}
+
+// Returns a promise resolving to the list of currently granted optional
+// permissions (i.e. excluding required permissions).
+export function grantedOptPermissions() {
+ return new Promise((resolve, reject) => {
+ chrome.permissions.getAll(response => {
+ if (response === undefined)
+ return reject(new Error(
+ chrome.runtime.lastError.message ??
+ 'An unknown error occurred while calling chrome.permissions.getAll()'));
+
+ let optPermissions =
+ response.permissions.filter(p => !requiredPermissions.has(p));
+ resolve(optPermissions);
+ });
+ });
+}
+
+// Returns a promise resolving to an object with 2 properties:
+// - missingPermissions: an array of optional permissions which are required
+// by enabled features and haven't been granted yet.
+// - leftoverPermissions: an array of optional permissions which are granted
+// but are no longer needed.
+export function diffPermissions() {
+ return Promise
+ .all([
+ grantedOptPermissions(),
+ currentRequiredOptPermissions(),
+ ])
+ .then(perms => {
+ return {
+ missingPermissions: perms[1].filter(p => !perms[0].includes(p)),
+ leftoverPermissions: perms[0].filter(p => !perms[1].includes(p)),
+ };
+ })
+ .catch(cause => {
+ throw new Error(
+ 'Couldn\'t compute the missing and leftover permissions.', {cause});
+ });
+}
+
+// Returns a promise which resolves to the array of required optional
+// permissions of |feature| which are missing.
+//
+// Accepts an argument |grantedPermissions| with the array of granted
+// permissions, otherwise the function will call grantedOptPermissions() to
+// retrieve them. This can be used to prevent calling
+// chrome.permissions.getAll() repeteadly.
+export function missingPermissions(feature, grantedPermissions = null) {
+ let grantedOptPermissionsPromise;
+ if (grantedPermissions !== null)
+ grantedOptPermissionsPromise = new Promise((res, rej) => {
+ res(grantedPermissions);
+ });
+ else
+ grantedOptPermissionsPromise = grantedOptPermissions();
+
+ return Promise
+ .all([
+ grantedOptPermissionsPromise,
+ requiredOptPermissions(feature),
+ ])
+ .then(perms => {
+ return perms[1].filter(p => !perms[0].includes(p));
+ })
+ .catch(cause => {
+ throw new Error(
+ 'Couldn\'t compute the missing permissions for "' + feature + '",',
+ {cause});
+ });
+}
+
+// Deletes optional permissions which are no longer needed by the current
+// set of enabled features (if |removeLeftoverPerms| is set to true), and sets a
+// badge if some needed permissions are missing.
+export function cleanUpOptPermissions(removeLeftoverPerms = true) {
+ return diffPermissions()
+ .then(perms => {
+ let {missingPermissions, leftoverPermissions} = perms;
+
+ if (missingPermissions.length > 0) {
+ actionApi.setBadgeBackgroundColor({color: '#B71C1C'});
+ actionApi.setBadgeText({text: '!'});
+ // This is to work around https://crbug.com/1268098.
+ // TODO(avm99963): Remove when the bug is fixed.
+ // #!if browser_target !== 'chromium_mv3'
+ actionApi.setTitle({
+ title: chrome.i18n.getMessage('actionbadge_permissions_requested')
+ });
+ // #!endif
+ } else {
+ actionApi.setBadgeText({text: ''});
+ actionApi.setTitle({title: ''});
+ }
+
+ if (removeLeftoverPerms) {
+ chrome.permissions.remove({
+ permissions: leftoverPermissions,
+ });
+ }
+ })
+ .catch(err => {
+ console.error(
+ 'An error ocurred while cleaning optional permissions: ', err);
+ });
+}
diff --git a/src/common/optionsUtils.js b/src/common/optionsUtils.js
index 9ead0b2..f4009ed 100644
--- a/src/common/optionsUtils.js
+++ b/src/common/optionsUtils.js
@@ -1,3 +1,4 @@
+import {grantedOptPermissions, missingPermissions} from './optionsPermissions.js';
import optionsPrototype from './optionsPrototype.json5';
import specialOptions from './specialOptions.json5';
@@ -27,40 +28,147 @@
return options;
}
-// Returns a promise which returns the values of options |options| which are
-// stored in the sync storage area.
-export function getOptions(options) {
- // Once we only target MV3, this can be greatly simplified.
- return new Promise((resolve, reject) => {
- if (typeof options === 'string')
- options = [options, '_forceDisabledFeatures'];
- else if (Array.isArray(options))
- options = [...options, '_forceDisabledFeatures'];
- else if (options !== null)
- console.error(
- 'Unexpected |options| parameter of type ' + (typeof options) +
- ' (expected: string, array, or null).');
+// This piece of code is used as part of the getOptions computation, and so
+// isn't that useful. It's exported since we sometimes need to finish the
+// computation in a service worker, where we have access to the
+// chrome.permissions API.
+//
+// It accepts as an argument an object |items| with the same structure of the
+// items saved in the sync storage area, and an array |permissionChecksFeatures|
+// of features
+export function disableItemsWithMissingPermissions(
+ items, permissionChecksFeatures) {
+ return grantedOptPermissions().then(grantedPerms => {
+ let permissionChecksPromises = [];
+ for (const f of permissionChecksFeatures)
+ permissionChecksPromises.push(missingPermissions(f, grantedPerms));
- chrome.storage.sync.get(options, items => {
- if (chrome.runtime.lastError) return reject(chrome.runtime.lastError);
+ Promise.all(permissionChecksPromises).then(missingPerms => {
+ for (let i = 0; i < permissionChecksFeatures.length; i++)
+ if (missingPerms[i].length > 0)
+ items[permissionChecksFeatures[i]] = false;
- // Handle applicable kill switches which force disable features
- if (items?._forceDisabledFeatures) {
- for (let feature of items?._forceDisabledFeatures) {
- items[feature] = false;
- }
-
- delete items._forceDisabledFeatures;
- }
-
- resolve(items);
+ return items;
});
});
}
+// Returns a promise which returns the values of options |options| which are
+// stored in the sync storage area.
+//
+// |requireOptionalPermissions| will determine whether to check if the required
+// optional permissions have been granted or not to the options which have such
+// requirements. If it is true, features with missing permissions will have
+// their value set to false.
+//
+// When a kill switch is active, affected options always have their value set to
+// false.
+
+// #!if !production
+let timerId = 0;
+let randomId = btoa(Math.random().toString()).substr(10, 5);
+// #!endif
+export function getOptions(options, requireOptionalPermissions = true) {
+ // #!if !production
+ let timeLabel = 'getOptions--' + randomId + '-' + (timerId++);
+ console.time(timeLabel);
+ // #!endif
+ // Once we only target MV3, this can be greatly simplified.
+ return new Promise((resolve, reject) => {
+ if (typeof options === 'string')
+ options = [options, '_forceDisabledFeatures'];
+ else if (Array.isArray(options))
+ options = [...options, '_forceDisabledFeatures'];
+ else if (options !== null)
+ return reject(new Error(
+ 'Unexpected |options| parameter of type ' + (typeof options) +
+ ' (expected: string, array, or null).'));
+
+ chrome.storage.sync.get(options, items => {
+ if (chrome.runtime.lastError)
+ return reject(chrome.runtime.lastError);
+
+ // Handle applicable kill switches which force disable features
+ if (items?._forceDisabledFeatures) {
+ for (let feature of items?._forceDisabledFeatures) {
+ items[feature] = false;
+ }
+
+ delete items._forceDisabledFeatures;
+ }
+
+ if (!requireOptionalPermissions) return resolve(items);
+
+ // Check whether some options have missing permissions which would
+ // force disable these features
+ let permissionChecksFeatures = [];
+ for (const [key, value] of Object.entries(items))
+ if ((key in optionsPrototype) && value &&
+ optionsPrototype[key].requiredOptPermissions?.length)
+ permissionChecksFeatures.push(key);
+
+ if (permissionChecksFeatures.length == 0) return resolve(items);
+
+ // If we don't have access to the chrome.permissions API (content
+ // scripts don't have access to it[1]), do the final piece of
+ // computation in the service worker/background script.
+ // [1]: https://developer.chrome.com/docs/extensions/mv3/content_scripts/
+
+ // #!if !production
+ console.debug('We are about to start checking granted permissions');
+ console.timeLog(timeLabel);
+ // #!endif
+ if (!chrome.permissions) {
+ return chrome.runtime.sendMessage(
+ {
+ message: 'runDisableItemsWithMissingPermissions',
+ options: {
+ items,
+ permissionChecksFeatures,
+ },
+ },
+ response => {
+ if (response === undefined)
+ return reject(new Error(
+ 'An error ocurred while communicating with the service worker: ' +
+ chrome.runtime.lastError.message));
+
+ if (response.status == 'rejected')
+ return reject(response.error);
+ if (response.status == 'resolved')
+ return resolve(response.items);
+ return reject(new Error(
+ 'An unknown response was recieved from service worker.'));
+ });
+ }
+
+ disableItemsWithMissingPermissions(items, permissionChecksFeatures)
+ .then(finalItems => resolve(finalItems))
+ .catch(err => reject(err));
+ });
+ })
+ // #!if !production
+ .then(items => {
+ console.group('getOptions(options); resolved; options: ', options);
+ console.timeEnd(timeLabel);
+ console.groupEnd();
+ return items;
+ })
+ .catch(err => {
+ console.group('getOptions(options); rejected; options: ', options);
+ console.timeEnd(timeLabel);
+ console.groupEnd();
+ throw err;
+ })
+ // #!endif
+ ;
+}
+
// Returns a promise which returns whether the |option| option/feature is
-// currently enabled.
-export function isOptionEnabled(option) {
+// currently enabled. If the feature requires optional permissions to work,
+// |requireOptionalPermissions| will determine whether to check if the required
+// optional permissions have been granted or not.
+export function isOptionEnabled(option, requireOptionalPermissions = true) {
return getOptions(option).then(options => {
return options?.[option] === true;
});
diff --git a/src/options/optionsCommon.js b/src/options/optionsCommon.js
index c76cd6c..b4bb42a 100644
--- a/src/options/optionsCommon.js
+++ b/src/options/optionsCommon.js
@@ -1,10 +1,13 @@
import {getExtVersion, isFirefox, isReleaseVersion} from '../common/extUtils.js';
+import {ensureOptPermissions, grantedOptPermissions, missingPermissions} from '../common/optionsPermissions.js';
import {cleanUpOptions, optionsPrototype, specialOptions} from '../common/optionsUtils.js';
+
import optionsPage from './optionsPage.json5';
var savedSuccessfullyTimeout = null;
const exclusiveOptions = [['thread', 'threadall']];
+const kClickShouldEnableFeat = 'data-click-should-enable-feature';
// Get a URL to a document which is part of the extension documentation (using
// |ref| as the Git ref).
@@ -142,14 +145,25 @@
optionsContainer.append(optionEl);
}
- // Add kill switch component after each option.
+ // Add optional permissions warning label and kill switch component
+ // after each option.
+ let optionalPermissionsWarningLabel = document.createElement('div');
+ optionalPermissionsWarningLabel.classList.add(
+ 'optional-permissions-warning-label');
+ optionalPermissionsWarningLabel.setAttribute('hidden', '');
+ optionalPermissionsWarningLabel.setAttribute(
+ 'data-feature', option.codename);
+ optionalPermissionsWarningLabel.setAttribute(
+ 'data-i18n', 'optionalpermissionswarning_label');
+
let killSwitchComponent = document.createElement('div');
killSwitchComponent.classList.add('kill-switch-label');
killSwitchComponent.setAttribute('hidden', '');
killSwitchComponent.setAttribute('data-feature', option.codename);
killSwitchComponent.setAttribute('data-i18n', 'killswitchenabled');
- optionsContainer.append(killSwitchComponent);
+ optionsContainer.append(
+ optionalPermissionsWarningLabel, killSwitchComponent);
}
}
@@ -178,9 +192,6 @@
document.getElementById('kill-switch-warning')
.removeAttribute('hidden');
}
-
- // TODO(avm99963): show a message above each option that has been force
- // disabled
}
for (var entry of Object.entries(optionsPrototype)) {
@@ -262,6 +273,84 @@
}
}));
});
+
+ // Handle options which need optional permissions.
+ grantedOptPermissions()
+ .then(grantedPerms => {
+ for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
+ if (!optMeta.requiredOptPermissions?.length || !isOptionShown(opt))
+ continue;
+
+ let warningLabel = document.querySelector(
+ '.optional-permissions-warning-label[data-feature="' + opt +
+ '"]');
+
+ // Ensure we have the appropriate permissions when the checkbox
+ // switches from disabled to enabled.
+ //
+ // Also, if the checkbox was indeterminate because the feature was
+ // enabled but not all permissions had been granted, enable the
+ // feature in order to trigger the permission request again.
+ let checkbox = document.getElementById(opt);
+ if (!checkbox) {
+ console.error('Expected checkbox for feature "' + opt + '".');
+ continue;
+ }
+ checkbox.addEventListener('change', () => {
+ if (checkbox.hasAttribute(kClickShouldEnableFeat)) {
+ checkbox.removeAttribute(kClickShouldEnableFeat);
+ checkbox.checked = true;
+ }
+
+ if (checkbox.checked)
+ ensureOptPermissions(opt)
+ .then(granted => {
+ if (granted) {
+ warningLabel.setAttribute('hidden', '');
+ if (!document.querySelector(
+ '.optional-permissions-warning-label:not([hidden])'))
+ document
+ .getElementById('optional-permissions-warning')
+ .setAttribute('hidden', '');
+ } else
+ document.getElementById('blockdrafts').checked = false;
+ })
+ .catch(err => {
+ console.error(
+ 'An error ocurred while ensuring that the optional ' +
+ 'permissions were granted after the checkbox ' +
+ 'was clicked for feature "' + opt + '":',
+ err);
+ document.getElementById('blockdrafts').checked = false;
+ });
+ });
+
+ // Add warning message if some permissions are missing and the
+ // feature is enabled.
+ if (items[opt] === true) {
+ let shownHeaderMessage = false;
+ missingPermissions(opt, grantedPerms)
+ .then(missingPerms => {
+ console.log(missingPerms);
+ if (missingPerms.length > 0) {
+ checkbox.indeterminate = true;
+ checkbox.setAttribute(kClickShouldEnableFeat, '');
+
+ warningLabel.removeAttribute('hidden');
+
+ if (!shownHeaderMessage) {
+ shownHeaderMessage = true;
+ document.getElementById('optional-permissions-warning')
+ .removeAttribute('hidden');
+ }
+ }
+ })
+ .catch(err => console.error(err));
+ }
+ }
+ })
+ .catch(err => console.error(err));
+
document.querySelector('#save').addEventListener('click', save);
});
});
diff --git a/src/static/_locales/en/messages.json b/src/static/_locales/en/messages.json
index 1fe26e9..9b4ec2e 100644
--- a/src/static/_locales/en/messages.json
+++ b/src/static/_locales/en/messages.json
@@ -23,6 +23,14 @@
"message": "The previous option has been force disabled due to an issue.",
"description": "Warning shown in the options page below an option, when it has been remotely force disabled via the kill switch mechanism."
},
+ "options_optionalpermissionswarning_header": {
+ "message": "One or more features can't be used because they need additional permissions. These features have been highlighted below, click their checkboxes to grant the appropriate permissions.",
+ "description": "Warning shown at the top of the options page if a feature cannot be used because one or more required permissions haven't been granted to the extension."
+ },
+ "options_optionalpermissionswarning_label": {
+ "message": "The previous feature needs additional permissions to work.",
+ "description": "Warning shown in the options page below an option, when a feature needs more permissions to work."
+ },
"options_featuredoptions": {
"message": "Featured options",
"description": "Heading for several options that can be enabled in the options page."
@@ -250,5 +258,9 @@
"inject_threadlistavatars_private_thread_indicator_label": {
"message": "Due to technical reasons, we can't load the avatars of threads published in private forums.",
"description": "Helper text which appears when hovering an icon next to a thread, to explain its meaning."
+ },
+ "actionbadge_permissions_requested": {
+ "message": "Some features need additional permissions to work. Click to fix it.",
+ "description": "Tooltip for the extension icon when a feature is enabled but it needs several permissions to be granted."
}
}
diff --git a/src/static/options/chrome_style/chrome_style.css b/src/static/options/chrome_style/chrome_style.css
index 4d3c91b..3f8b076 100644
--- a/src/static/options/chrome_style/chrome_style.css
+++ b/src/static/options/chrome_style/chrome_style.css
@@ -84,7 +84,6 @@
select,
input[type='checkbox'],
input[type='radio'] {
- -webkit-appearance: none;
-webkit-user-select: none;
background-image: linear-gradient(#ededed, #ededed 38%, #dedede);
border: 1px solid rgba(0, 0, 0, 0.25);
@@ -98,6 +97,12 @@
text-shadow: 0 1px 0 rgb(240, 240, 240);
}
+select,
+input[type='checkbox']:not(:indeterminate),
+input[type='radio'] {
+ -webkit-appearance: none;
+}
+
:-webkit-any(button,
input[type='button'],
input[type='submit']),
@@ -173,7 +178,7 @@
/* Checked ********************************************************************/
-input[type='checkbox']:checked::before {
+input[type='checkbox']:checked:not(:indeterminate)::before {
-webkit-user-select: none;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAQAAAADpb+tAAAAZ0lEQVR4AWNAA2xAiAXEM8xiMEAXVGJYz7AZCFEkmBi6wYKtEC4/gxqY9gILrmYQhwiXMWxkiAVyVoOFfSCCpkAmCK4Fk+1QA4GqekECUAMkka0KY9gIFvZDd5oawwyGBqACdIDqOwAQzBnTWnnU+gAAAABJRU5ErkJggg==);
background-size: 100% 100%;
diff --git a/src/static/options/experiments.html b/src/static/options/experiments.html
index f5452fa..0f42f11 100644
--- a/src/static/options/experiments.html
+++ b/src/static/options/experiments.html
@@ -12,6 +12,7 @@
<h1 data-i18n="experiments_title"></h1>
<p data-i18n="experiments_description"></p>
<form>
+ <div id="optional-permissions-warning" hidden data-i18n="optionalpermissionswarning_header"></div>
<div class="actions"><button id="save" data-i18n="save"></button></div>
</form>
<div id="save-indicator"></div>
diff --git a/src/static/options/options.css b/src/static/options/options.css
index ce79b81..68057ce 100644
--- a/src/static/options/options.css
+++ b/src/static/options/options.css
@@ -24,16 +24,22 @@
cursor: pointer;
}
-.option, #kill-switch-warning {
+.option,
+ #kill-switch-warning,
+ #optional-permissions-warning {
margin: 4px 0;
line-height: 1.8em;
}
-#kill-switch-warning, .kill-switch-label {
+#kill-switch-warning,
+ .kill-switch-label,
+ #optional-permissions-warning,
+ .optional-permissions-warning-label {
color: red;
}
-.kill-switch-label {
+.kill-switch-label,
+ .optional-permissions-warning-label {
margin: 0 0 8px 0;
line-height: 1em;
}
diff --git a/src/static/options/options.html b/src/static/options/options.html
index 8a0915a..36fd08a 100644
--- a/src/static/options/options.html
+++ b/src/static/options/options.html
@@ -39,6 +39,7 @@
</a>
</div>
<form>
+ <div id="optional-permissions-warning" hidden data-i18n="optionalpermissionswarning_header"></div>
<div id="kill-switch-warning" hidden data-i18n="killswitchwarning"></div>
<div id="options-container"></div>
<div class="actions"><button id="save" data-i18n="save"></button></div>
diff --git a/src/sw.js b/src/sw.js
index 73efa06..fce78d2 100644
--- a/src/sw.js
+++ b/src/sw.js
@@ -1,7 +1,8 @@
// IMPORTANT: keep this file in sync with background.js
import XMLHttpRequest from 'sw-xhr';
-import {cleanUpOptions} from './common/optionsUtils.js';
+import {cleanUpOptPermissions} from './common/optionsPermissions.js';
+import {cleanUpOptions, disableItemsWithMissingPermissions} from './common/optionsUtils.js';
import KillSwitchMechanism from './killSwitch/index.js';
// XMLHttpRequest is not present in service workers and is required by the
@@ -40,3 +41,34 @@
killSwitchMechanism.updateKillSwitchStatus();
}
});
+
+// Clean up optional permissions and check that none are missing for enabled
+// features as soon as the extension starts and when the options change.
+cleanUpOptPermissions();
+
+chrome.storage.sync.onChanged.addListener(() => {
+ cleanUpOptPermissions();
+});
+
+chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
+ if (sender.id !== chrome.runtime.id)
+ return console.warn(
+ 'An unknown sender (' + sender.id +
+ ') sent a message to the extension: ',
+ msg);
+
+ console.assert(msg.message);
+ switch (msg.message) {
+ case 'runDisableItemsWithMissingPermissions':
+ console.assert(
+ msg.options?.items && msg.options?.permissionChecksFeatures);
+ disableItemsWithMissingPermissions(
+ msg.options?.items, msg.options?.permissionChecksFeatures)
+ .then(items => sendResponse({status: 'resolved', items}))
+ .catch(error => sendResponse({status: 'rejected', error}));
+ break;
+
+ default:
+ console.warn('Unknown message "' + msg.message + '".');
+ }
+});
diff --git a/webpack.config.js b/webpack.config.js
index 2c94b12..660d631 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -121,6 +121,20 @@
},
],
},
+ {
+ test: /\.js$/i,
+ use: [
+ {
+ loader: 'webpack-preprocessor-loader',
+ options: {
+ params: {
+ browser_target: env.browser_target,
+ production: args.mode == 'production',
+ },
+ },
+ },
+ ],
+ },
]
},
};