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/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;
});