blob: 1bfd082bb085443830bf2d8c5ed9d8a7b0c5213f [file] [log] [blame]
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +02001import actionApi from './actionApi.js';
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +01002import optionsPrototype from './optionsPrototype.json5';
3import {getOptions} from './optionsUtils.js';
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +01004
5// Required permissions, including host permissions.
6//
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +02007// IMPORTANT: This should be kept in sync with the "permissions",
8// "host_permissions" and "content_scripts" keys in //templates/manifest.gjson.
9const requiredPermissions = {
10 permissions: new Set([
11 'storage', 'alarms',
12 // #!if ['chromium', 'chromium_mv3'].includes(browser_target)
13 'declarativeNetRequestWithHostAccess',
14 // #!endif
15 ]),
16 origins: new Set([
17 // Host permissions:
18 'https://support.google.com/*',
19
20 // Content scripts matches:
21 'https://support.google.com/s/community*',
22 'https://support.google.com/*/threads*',
23 'https://support.google.com/*/thread/*',
24 'https://support.google.com/*/profile/*',
25 'https://support.google.com/profile/*',
26 ]),
27};
28
29const permissionTypes = ['origins', 'permissions'];
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010030
31// Returns an array of optional permissions needed by |feature|.
32export function requiredOptPermissions(feature) {
33 if (!(feature in optionsPrototype)) {
34 console.error('"' + feature + '" feature doesn\'t exist.');
35 return [];
36 }
37
38 return optionsPrototype[feature]?.requiredOptPermissions ?? [];
39}
40
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020041// Returns a promise resolving to the optional permissions needed by all the
42// current enabled features.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010043export function currentRequiredOptPermissions() {
44 return getOptions(null, /* requireOptionalPermissions = */ false)
45 .then(options => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020046 let permissions = {
47 origins: [],
48 permissions: [],
49 };
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010050
51 // For each option
52 for (const [opt, optMeta] of Object.entries(optionsPrototype))
53 // If the option is enabled
54 if (options[opt])
55 // Add its required optional permissions to the list
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020056 for (const type of permissionTypes)
57 permissions[type].push(
58 ...(optMeta.requiredOptPermissions?.[type] ?? []));
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010059
60 return permissions;
61 });
62}
63
64// Ensures that all the optional permissions required by |feature| are granted,
65// and requests them otherwise. It returns a promise which resolves specifying
66// whether the permissions were granted or not.
67export function ensureOptPermissions(feature) {
68 return new Promise((resolve, reject) => {
69 let permissions = requiredOptPermissions(feature);
70
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020071 chrome.permissions.contains(permissions, isAlreadyGranted => {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010072 if (isAlreadyGranted) return resolve(true);
73
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020074 chrome.permissions.request(permissions, granted => {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010075 // If there was an error, reject the promise.
76 if (granted === undefined)
77 return reject(new Error(
78 chrome.runtime.lastError.message ??
79 'An unknown error occurred while requesting the permisisons'));
80
81 // If the permission is granted we should maybe remove the warning
82 // badge.
83 if (granted) cleanUpOptPermissions(/* removeLeftoverPerms = */ false);
84
85 return resolve(granted);
86 });
87 });
88 });
89}
90
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020091// Returns a promise resolving to the currently granted optional permissions
92// (i.e. excluding required permissions).
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010093export function grantedOptPermissions() {
94 return new Promise((resolve, reject) => {
95 chrome.permissions.getAll(response => {
96 if (response === undefined)
97 return reject(new Error(
98 chrome.runtime.lastError.message ??
99 'An unknown error occurred while calling chrome.permissions.getAll()'));
100
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200101 let optPermissions = {};
102 for (const type of permissionTypes)
103 optPermissions[type] =
104 response[type].filter(p => !requiredPermissions[type].has(p));
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100105 resolve(optPermissions);
106 });
107 });
108}
109
110// Returns a promise resolving to an object with 2 properties:
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200111// - missingPermissions: optional permissions which are required by enabled
112// features and haven't been granted yet.
113// - leftoverPermissions: optional permissions which are granted but are no
114// longer needed.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100115export function diffPermissions() {
116 return Promise
117 .all([
118 grantedOptPermissions(),
119 currentRequiredOptPermissions(),
120 ])
121 .then(perms => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200122 let diff = {
123 missingPermissions: {},
124 leftoverPermissions: {},
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100125 };
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200126 for (const type of permissionTypes) {
127 diff.missingPermissions[type] =
128 perms[1][type].filter(p => !perms[0][type].includes(p));
129 diff.leftoverPermissions[type] =
130 perms[0][type].filter(p => !perms[1][type].includes(p));
131 }
132 return diff;
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100133 })
134 .catch(cause => {
135 throw new Error(
136 'Couldn\'t compute the missing and leftover permissions.', {cause});
137 });
138}
139
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200140// Returns a promise which resolves to the required optional permissions of
141// |feature| which are missing.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100142//
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200143// Accepts an argument |grantedPermissions| with the granted permissions,
144// otherwise the function will call grantedOptPermissions() to retrieve them.
145// This can be used to prevent calling chrome.permissions.getAll() repeteadly.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100146export function missingPermissions(feature, grantedPermissions = null) {
147 let grantedOptPermissionsPromise;
148 if (grantedPermissions !== null)
149 grantedOptPermissionsPromise = new Promise((res, rej) => {
150 res(grantedPermissions);
151 });
152 else
153 grantedOptPermissionsPromise = grantedOptPermissions();
154
155 return Promise
156 .all([
157 grantedOptPermissionsPromise,
158 requiredOptPermissions(feature),
159 ])
160 .then(perms => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200161 let missingPerms = {};
162 for (const type of permissionTypes)
163 missingPerms[type] =
164 perms[1][type].filter(p => !perms[0][type].includes(p))
165 return missingPerms;
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100166 })
167 .catch(cause => {
168 throw new Error(
169 'Couldn\'t compute the missing permissions for "' + feature + '",',
170 {cause});
171 });
172}
173
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200174// Returns true if permissions (a chrome.permissions.Permissions object) is
175// empty (that is, if their properties have empty arrays).
176export function isPermissionsObjectEmpty(permissions) {
177 for (const type of permissionTypes) {
178 if ((permissions[type]?.length ?? 0) > 0) return false;
179 }
180 return true;
181}
182
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100183// Deletes optional permissions which are no longer needed by the current
184// set of enabled features (if |removeLeftoverPerms| is set to true), and sets a
185// badge if some needed permissions are missing.
186export function cleanUpOptPermissions(removeLeftoverPerms = true) {
187 return diffPermissions()
188 .then(perms => {
189 let {missingPermissions, leftoverPermissions} = perms;
190
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200191 if (!isPermissionsObjectEmpty(missingPermissions)) {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100192 actionApi.setBadgeBackgroundColor({color: '#B71C1C'});
193 actionApi.setBadgeText({text: '!'});
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100194 actionApi.setTitle({
195 title: chrome.i18n.getMessage('actionbadge_permissions_requested')
196 });
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100197 } else {
198 actionApi.setBadgeText({text: ''});
199 actionApi.setTitle({title: ''});
200 }
201
202 if (removeLeftoverPerms) {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200203 chrome.permissions.remove(leftoverPermissions);
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100204 }
205 })
206 .catch(err => {
207 console.error(
208 'An error ocurred while cleaning optional permissions: ', err);
209 });
210}