blob: bb84a882dee329eeebe5a20a14d4044341288dbb [file] [log] [blame]
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +02001import actionApi from './actionApi.js';
Adrià Vilanova Martíneza2ede8c2024-04-21 16:43:01 +02002import {optionsPrototype} from './optionsPrototype.ts';
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +01003import {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
Adrià Vilanova Martínez5f5b3e02023-07-23 00:08:17 +020015 // #!if browser_target == 'chromium_mv3'
16 'scripting',
17 // #!endif
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020018 ]),
19 origins: new Set([
20 // Host permissions:
21 'https://support.google.com/*',
22
23 // Content scripts matches:
24 'https://support.google.com/s/community*',
25 'https://support.google.com/*/threads*',
26 'https://support.google.com/*/thread/*',
Adrià Vilanova Martínez0f508c92024-02-28 23:45:22 +010027 'https://support.google.com/*/community-guide/*',
28 'https://support.google.com/*/community-video/*',
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020029 'https://support.google.com/*/profile/*',
30 'https://support.google.com/profile/*',
31 ]),
32};
33
34const permissionTypes = ['origins', 'permissions'];
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010035
36// Returns an array of optional permissions needed by |feature|.
37export function requiredOptPermissions(feature) {
38 if (!(feature in optionsPrototype)) {
39 console.error('"' + feature + '" feature doesn\'t exist.');
40 return [];
41 }
42
43 return optionsPrototype[feature]?.requiredOptPermissions ?? [];
44}
45
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020046// Returns a promise resolving to the optional permissions needed by all the
47// current enabled features.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010048export function currentRequiredOptPermissions() {
49 return getOptions(null, /* requireOptionalPermissions = */ false)
50 .then(options => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020051 let permissions = {
52 origins: [],
53 permissions: [],
54 };
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010055
56 // For each option
57 for (const [opt, optMeta] of Object.entries(optionsPrototype))
58 // If the option is enabled
59 if (options[opt])
60 // Add its required optional permissions to the list
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020061 for (const type of permissionTypes)
62 permissions[type].push(
63 ...(optMeta.requiredOptPermissions?.[type] ?? []));
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010064
65 return permissions;
66 });
67}
68
69// Ensures that all the optional permissions required by |feature| are granted,
70// and requests them otherwise. It returns a promise which resolves specifying
71// whether the permissions were granted or not.
72export function ensureOptPermissions(feature) {
73 return new Promise((resolve, reject) => {
74 let permissions = requiredOptPermissions(feature);
75
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020076 chrome.permissions.contains(permissions, isAlreadyGranted => {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010077 if (isAlreadyGranted) return resolve(true);
78
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020079 chrome.permissions.request(permissions, granted => {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010080 // If there was an error, reject the promise.
81 if (granted === undefined)
82 return reject(new Error(
83 chrome.runtime.lastError.message ??
84 'An unknown error occurred while requesting the permisisons'));
85
86 // If the permission is granted we should maybe remove the warning
87 // badge.
88 if (granted) cleanUpOptPermissions(/* removeLeftoverPerms = */ false);
89
90 return resolve(granted);
91 });
92 });
93 });
94}
95
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020096// Returns a promise resolving to the currently granted optional permissions
97// (i.e. excluding required permissions).
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010098export function grantedOptPermissions() {
99 return new Promise((resolve, reject) => {
100 chrome.permissions.getAll(response => {
101 if (response === undefined)
102 return reject(new Error(
103 chrome.runtime.lastError.message ??
104 'An unknown error occurred while calling chrome.permissions.getAll()'));
105
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200106 let optPermissions = {};
107 for (const type of permissionTypes)
108 optPermissions[type] =
109 response[type].filter(p => !requiredPermissions[type].has(p));
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100110 resolve(optPermissions);
111 });
112 });
113}
114
115// Returns a promise resolving to an object with 2 properties:
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200116// - missingPermissions: optional permissions which are required by enabled
117// features and haven't been granted yet.
118// - leftoverPermissions: optional permissions which are granted but are no
119// longer needed.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100120export function diffPermissions() {
121 return Promise
122 .all([
123 grantedOptPermissions(),
124 currentRequiredOptPermissions(),
125 ])
126 .then(perms => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200127 let diff = {
128 missingPermissions: {},
129 leftoverPermissions: {},
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100130 };
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200131 for (const type of permissionTypes) {
132 diff.missingPermissions[type] =
133 perms[1][type].filter(p => !perms[0][type].includes(p));
134 diff.leftoverPermissions[type] =
135 perms[0][type].filter(p => !perms[1][type].includes(p));
136 }
137 return diff;
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100138 })
139 .catch(cause => {
140 throw new Error(
141 'Couldn\'t compute the missing and leftover permissions.', {cause});
142 });
143}
144
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200145// Returns a promise which resolves to the required optional permissions of
146// |feature| which are missing.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100147//
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200148// Accepts an argument |grantedPermissions| with the granted permissions,
149// otherwise the function will call grantedOptPermissions() to retrieve them.
150// This can be used to prevent calling chrome.permissions.getAll() repeteadly.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100151export function missingPermissions(feature, grantedPermissions = null) {
152 let grantedOptPermissionsPromise;
153 if (grantedPermissions !== null)
154 grantedOptPermissionsPromise = new Promise((res, rej) => {
155 res(grantedPermissions);
156 });
157 else
158 grantedOptPermissionsPromise = grantedOptPermissions();
159
160 return Promise
161 .all([
162 grantedOptPermissionsPromise,
163 requiredOptPermissions(feature),
164 ])
165 .then(perms => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200166 let missingPerms = {};
167 for (const type of permissionTypes)
168 missingPerms[type] =
169 perms[1][type].filter(p => !perms[0][type].includes(p))
170 return missingPerms;
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100171 })
172 .catch(cause => {
173 throw new Error(
174 'Couldn\'t compute the missing permissions for "' + feature + '",',
175 {cause});
176 });
177}
178
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200179// Returns true if permissions (a chrome.permissions.Permissions object) is
180// empty (that is, if their properties have empty arrays).
181export function isPermissionsObjectEmpty(permissions) {
182 for (const type of permissionTypes) {
183 if ((permissions[type]?.length ?? 0) > 0) return false;
184 }
185 return true;
186}
187
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100188// Deletes optional permissions which are no longer needed by the current
189// set of enabled features (if |removeLeftoverPerms| is set to true), and sets a
190// badge if some needed permissions are missing.
191export function cleanUpOptPermissions(removeLeftoverPerms = true) {
192 return diffPermissions()
193 .then(perms => {
194 let {missingPermissions, leftoverPermissions} = perms;
195
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200196 if (!isPermissionsObjectEmpty(missingPermissions)) {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100197 actionApi.setBadgeBackgroundColor({color: '#B71C1C'});
198 actionApi.setBadgeText({text: '!'});
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100199 actionApi.setTitle({
200 title: chrome.i18n.getMessage('actionbadge_permissions_requested')
201 });
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100202 } else {
203 actionApi.setBadgeText({text: ''});
204 actionApi.setTitle({title: ''});
205 }
206
207 if (removeLeftoverPerms) {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200208 chrome.permissions.remove(leftoverPermissions);
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100209 }
210 })
211 .catch(err => {
212 console.error(
213 'An error ocurred while cleaning optional permissions: ', err);
214 });
215}