blob: 1bfd082bb085443830bf2d8c5ed9d8a7b0c5213f [file] [log] [blame]
import actionApi from './actionApi.js';
import optionsPrototype from './optionsPrototype.json5';
import {getOptions} from './optionsUtils.js';
// Required permissions, including host permissions.
//
// IMPORTANT: This should be kept in sync with the "permissions",
// "host_permissions" and "content_scripts" keys in //templates/manifest.gjson.
const requiredPermissions = {
permissions: new Set([
'storage', 'alarms',
// #!if ['chromium', 'chromium_mv3'].includes(browser_target)
'declarativeNetRequestWithHostAccess',
// #!endif
]),
origins: new Set([
// Host permissions:
'https://support.google.com/*',
// Content scripts matches:
'https://support.google.com/s/community*',
'https://support.google.com/*/threads*',
'https://support.google.com/*/thread/*',
'https://support.google.com/*/profile/*',
'https://support.google.com/profile/*',
]),
};
const permissionTypes = ['origins', 'permissions'];
// 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 the optional permissions needed by all the
// current enabled features.
export function currentRequiredOptPermissions() {
return getOptions(null, /* requireOptionalPermissions = */ false)
.then(options => {
let permissions = {
origins: [],
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
for (const type of permissionTypes)
permissions[type].push(
...(optMeta.requiredOptPermissions?.[type] ?? []));
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 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 = {};
for (const type of permissionTypes)
optPermissions[type] =
response[type].filter(p => !requiredPermissions[type].has(p));
resolve(optPermissions);
});
});
}
// Returns a promise resolving to an object with 2 properties:
// - missingPermissions: optional permissions which are required by enabled
// features and haven't been granted yet.
// - leftoverPermissions: optional permissions which are granted but are no
// longer needed.
export function diffPermissions() {
return Promise
.all([
grantedOptPermissions(),
currentRequiredOptPermissions(),
])
.then(perms => {
let diff = {
missingPermissions: {},
leftoverPermissions: {},
};
for (const type of permissionTypes) {
diff.missingPermissions[type] =
perms[1][type].filter(p => !perms[0][type].includes(p));
diff.leftoverPermissions[type] =
perms[0][type].filter(p => !perms[1][type].includes(p));
}
return diff;
})
.catch(cause => {
throw new Error(
'Couldn\'t compute the missing and leftover permissions.', {cause});
});
}
// Returns a promise which resolves to the required optional permissions of
// |feature| which are missing.
//
// Accepts an argument |grantedPermissions| with the 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 => {
let missingPerms = {};
for (const type of permissionTypes)
missingPerms[type] =
perms[1][type].filter(p => !perms[0][type].includes(p))
return missingPerms;
})
.catch(cause => {
throw new Error(
'Couldn\'t compute the missing permissions for "' + feature + '",',
{cause});
});
}
// Returns true if permissions (a chrome.permissions.Permissions object) is
// empty (that is, if their properties have empty arrays).
export function isPermissionsObjectEmpty(permissions) {
for (const type of permissionTypes) {
if ((permissions[type]?.length ?? 0) > 0) return false;
}
return true;
}
// 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 (!isPermissionsObjectEmpty(missingPermissions)) {
actionApi.setBadgeBackgroundColor({color: '#B71C1C'});
actionApi.setBadgeText({text: '!'});
actionApi.setTitle({
title: chrome.i18n.getMessage('actionbadge_permissions_requested')
});
} else {
actionApi.setBadgeText({text: ''});
actionApi.setTitle({title: ''});
}
if (removeLeftoverPerms) {
chrome.permissions.remove(leftoverPermissions);
}
})
.catch(err => {
console.error(
'An error ocurred while cleaning optional permissions: ', err);
});
}