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