blob: 055e46bb2ec986c6ce81da90f33ebe58ca7093b3 [file] [log] [blame]
import {grantedOptPermissions, isPermissionsObjectEmpty, missingPermissions} from './optionsPermissions.js';
import {optionsPrototype} from './optionsPrototype';
import specialOptions from './specialOptions.json5';
export {optionsPrototype, specialOptions};
// Adds missing options with their default value. If |dryRun| is set to false,
// they are also saved to the sync storage area.
export function cleanUpOptions(options, dryRun = false) {
console.debug('[cleanUpOptions] Previous options', JSON.stringify(options));
if (typeof options !== 'object' || options === null) options = {};
var ok = true;
for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
if (!(opt in options) &&
optMeta['killSwitchType'] !== 'internalKillSwitch') {
ok = false;
options[opt] = optMeta['defaultValue'];
}
}
console.debug('[cleanUpOptions] New options', JSON.stringify(options));
if (!ok && !dryRun) {
chrome.storage.sync.set(options);
}
return options;
}
// 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));
Promise.all(permissionChecksPromises).then(missingPerms => {
for (let i = 0; i < permissionChecksFeatures.length; i++)
if (!isPermissionsObjectEmpty(missingPerms[i]))
items[permissionChecksFeatures[i]] = false;
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. 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, requireOptionalPermissions).then(options => {
return options?.[option] === true;
});
}
export function getForceDisabledFeatures() {
return new Promise((res, rej) => {
chrome.storage.sync.get('_forceDisabledFeatures', items => {
if (chrome.runtime.lastError) return rej(chrome.runtime.lastError);
res(items?.['_forceDisabledFeatures'] ?? []);
});
});
}