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