blob: 9a998ad7a5bc4b6d5d7c022aed6f71ece7501932 [file] [log] [blame]
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +01001import optionsPrototype from './optionsPrototype.json5';
2import {getOptions} from './optionsUtils.js';
3
4// #!if browser_target == 'chromium_mv3'
5const actionApi = chrome.action;
6// #!else
7const actionApi = chrome.browserAction;
8// #!endif
9
10// Required permissions, including host permissions.
11//
12// IMPORTANT: This should be kept in sync with the "permissions" key in
13// //templates/manifest.gjson.
14const requiredPermissions = new Set([
15 'storage',
16 'alarms',
17]);
18
19// Returns an array of optional permissions needed by |feature|.
20export function requiredOptPermissions(feature) {
21 if (!(feature in optionsPrototype)) {
22 console.error('"' + feature + '" feature doesn\'t exist.');
23 return [];
24 }
25
26 return optionsPrototype[feature]?.requiredOptPermissions ?? [];
27}
28
29// Returns a promise resolving to an array of optional permissions needed by all
30// the current enabled features.
31export function currentRequiredOptPermissions() {
32 return getOptions(null, /* requireOptionalPermissions = */ false)
33 .then(options => {
34 let permissions = [];
35
36 // For each option
37 for (const [opt, optMeta] of Object.entries(optionsPrototype))
38 // If the option is enabled
39 if (options[opt])
40 // Add its required optional permissions to the list
41 permissions.push(...(optMeta.requiredOptPermissions ?? []));
42
43 return permissions;
44 });
45}
46
47// Ensures that all the optional permissions required by |feature| are granted,
48// and requests them otherwise. It returns a promise which resolves specifying
49// whether the permissions were granted or not.
50export function ensureOptPermissions(feature) {
51 return new Promise((resolve, reject) => {
52 let permissions = requiredOptPermissions(feature);
53
54 chrome.permissions.contains({permissions}, isAlreadyGranted => {
55 if (isAlreadyGranted) return resolve(true);
56
57 chrome.permissions.request({permissions}, granted => {
58 // If there was an error, reject the promise.
59 if (granted === undefined)
60 return reject(new Error(
61 chrome.runtime.lastError.message ??
62 'An unknown error occurred while requesting the permisisons'));
63
64 // If the permission is granted we should maybe remove the warning
65 // badge.
66 if (granted) cleanUpOptPermissions(/* removeLeftoverPerms = */ false);
67
68 return resolve(granted);
69 });
70 });
71 });
72}
73
74// Returns a promise resolving to the list of currently granted optional
75// permissions (i.e. excluding required permissions).
76export function grantedOptPermissions() {
77 return new Promise((resolve, reject) => {
78 chrome.permissions.getAll(response => {
79 if (response === undefined)
80 return reject(new Error(
81 chrome.runtime.lastError.message ??
82 'An unknown error occurred while calling chrome.permissions.getAll()'));
83
84 let optPermissions =
85 response.permissions.filter(p => !requiredPermissions.has(p));
86 resolve(optPermissions);
87 });
88 });
89}
90
91// Returns a promise resolving to an object with 2 properties:
92// - missingPermissions: an array of optional permissions which are required
93// by enabled features and haven't been granted yet.
94// - leftoverPermissions: an array of optional permissions which are granted
95// but are no longer needed.
96export function diffPermissions() {
97 return Promise
98 .all([
99 grantedOptPermissions(),
100 currentRequiredOptPermissions(),
101 ])
102 .then(perms => {
103 return {
104 missingPermissions: perms[1].filter(p => !perms[0].includes(p)),
105 leftoverPermissions: perms[0].filter(p => !perms[1].includes(p)),
106 };
107 })
108 .catch(cause => {
109 throw new Error(
110 'Couldn\'t compute the missing and leftover permissions.', {cause});
111 });
112}
113
114// Returns a promise which resolves to the array of required optional
115// permissions of |feature| which are missing.
116//
117// Accepts an argument |grantedPermissions| with the array of granted
118// permissions, otherwise the function will call grantedOptPermissions() to
119// retrieve them. This can be used to prevent calling
120// chrome.permissions.getAll() repeteadly.
121export function missingPermissions(feature, grantedPermissions = null) {
122 let grantedOptPermissionsPromise;
123 if (grantedPermissions !== null)
124 grantedOptPermissionsPromise = new Promise((res, rej) => {
125 res(grantedPermissions);
126 });
127 else
128 grantedOptPermissionsPromise = grantedOptPermissions();
129
130 return Promise
131 .all([
132 grantedOptPermissionsPromise,
133 requiredOptPermissions(feature),
134 ])
135 .then(perms => {
136 return perms[1].filter(p => !perms[0].includes(p));
137 })
138 .catch(cause => {
139 throw new Error(
140 'Couldn\'t compute the missing permissions for "' + feature + '",',
141 {cause});
142 });
143}
144
145// Deletes optional permissions which are no longer needed by the current
146// set of enabled features (if |removeLeftoverPerms| is set to true), and sets a
147// badge if some needed permissions are missing.
148export function cleanUpOptPermissions(removeLeftoverPerms = true) {
149 return diffPermissions()
150 .then(perms => {
151 let {missingPermissions, leftoverPermissions} = perms;
152
153 if (missingPermissions.length > 0) {
154 actionApi.setBadgeBackgroundColor({color: '#B71C1C'});
155 actionApi.setBadgeText({text: '!'});
156 // This is to work around https://crbug.com/1268098.
157 // TODO(avm99963): Remove when the bug is fixed.
158 // #!if browser_target !== 'chromium_mv3'
159 actionApi.setTitle({
160 title: chrome.i18n.getMessage('actionbadge_permissions_requested')
161 });
162 // #!endif
163 } else {
164 actionApi.setBadgeText({text: ''});
165 actionApi.setTitle({title: ''});
166 }
167
168 if (removeLeftoverPerms) {
169 chrome.permissions.remove({
170 permissions: leftoverPermissions,
171 });
172 }
173 })
174 .catch(err => {
175 console.error(
176 'An error ocurred while cleaning optional permissions: ', err);
177 });
178}