blob: 2fa298b2944a33cd8e2f6ea7a87bd7d29f054631 [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',
Adrià Vilanova Martínez5f5b3e02023-07-23 00:08:17 +020012 // #!if browser_target == 'chromium_mv3'
13 'scripting',
14 // #!endif
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020015 ]),
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/*',
Adrià Vilanova Martínez0f508c92024-02-28 23:45:22 +010024 'https://support.google.com/*/community-guide/*',
25 'https://support.google.com/*/community-video/*',
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020026 'https://support.google.com/*/profile/*',
27 'https://support.google.com/profile/*',
28 ]),
29};
30
31const permissionTypes = ['origins', 'permissions'];
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010032
33// Returns an array of optional permissions needed by |feature|.
34export function requiredOptPermissions(feature) {
35 if (!(feature in optionsPrototype)) {
36 console.error('"' + feature + '" feature doesn\'t exist.');
37 return [];
38 }
39
40 return optionsPrototype[feature]?.requiredOptPermissions ?? [];
41}
42
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020043// Returns a promise resolving to the optional permissions needed by all the
44// current enabled features.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010045export function currentRequiredOptPermissions() {
46 return getOptions(null, /* requireOptionalPermissions = */ false)
47 .then(options => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020048 let permissions = {
49 origins: [],
50 permissions: [],
51 };
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010052
53 // For each option
54 for (const [opt, optMeta] of Object.entries(optionsPrototype))
55 // If the option is enabled
56 if (options[opt])
57 // Add its required optional permissions to the list
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020058 for (const type of permissionTypes)
59 permissions[type].push(
60 ...(optMeta.requiredOptPermissions?.[type] ?? []));
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010061
62 return permissions;
63 });
64}
65
66// Ensures that all the optional permissions required by |feature| are granted,
67// and requests them otherwise. It returns a promise which resolves specifying
68// whether the permissions were granted or not.
69export function ensureOptPermissions(feature) {
70 return new Promise((resolve, reject) => {
71 let permissions = requiredOptPermissions(feature);
72
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020073 chrome.permissions.contains(permissions, isAlreadyGranted => {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010074 if (isAlreadyGranted) return resolve(true);
75
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020076 chrome.permissions.request(permissions, granted => {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010077 // If there was an error, reject the promise.
78 if (granted === undefined)
79 return reject(new Error(
80 chrome.runtime.lastError.message ??
81 'An unknown error occurred while requesting the permisisons'));
82
83 // If the permission is granted we should maybe remove the warning
84 // badge.
85 if (granted) cleanUpOptPermissions(/* removeLeftoverPerms = */ false);
86
87 return resolve(granted);
88 });
89 });
90 });
91}
92
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020093// Returns a promise resolving to the currently granted optional permissions
94// (i.e. excluding required permissions).
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010095export function grantedOptPermissions() {
96 return new Promise((resolve, reject) => {
97 chrome.permissions.getAll(response => {
98 if (response === undefined)
99 return reject(new Error(
100 chrome.runtime.lastError.message ??
101 'An unknown error occurred while calling chrome.permissions.getAll()'));
102
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200103 let optPermissions = {};
104 for (const type of permissionTypes)
105 optPermissions[type] =
106 response[type].filter(p => !requiredPermissions[type].has(p));
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100107 resolve(optPermissions);
108 });
109 });
110}
111
112// Returns a promise resolving to an object with 2 properties:
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200113// - missingPermissions: optional permissions which are required by enabled
114// features and haven't been granted yet.
115// - leftoverPermissions: optional permissions which are granted but are no
116// longer needed.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100117export function diffPermissions() {
118 return Promise
119 .all([
120 grantedOptPermissions(),
121 currentRequiredOptPermissions(),
122 ])
123 .then(perms => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200124 let diff = {
125 missingPermissions: {},
126 leftoverPermissions: {},
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100127 };
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200128 for (const type of permissionTypes) {
129 diff.missingPermissions[type] =
130 perms[1][type].filter(p => !perms[0][type].includes(p));
131 diff.leftoverPermissions[type] =
132 perms[0][type].filter(p => !perms[1][type].includes(p));
133 }
134 return diff;
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100135 })
136 .catch(cause => {
137 throw new Error(
138 'Couldn\'t compute the missing and leftover permissions.', {cause});
139 });
140}
141
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200142// Returns a promise which resolves to the required optional permissions of
143// |feature| which are missing.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100144//
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200145// Accepts an argument |grantedPermissions| with the granted permissions,
146// otherwise the function will call grantedOptPermissions() to retrieve them.
147// This can be used to prevent calling chrome.permissions.getAll() repeteadly.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100148export function missingPermissions(feature, grantedPermissions = null) {
149 let grantedOptPermissionsPromise;
150 if (grantedPermissions !== null)
151 grantedOptPermissionsPromise = new Promise((res, rej) => {
152 res(grantedPermissions);
153 });
154 else
155 grantedOptPermissionsPromise = grantedOptPermissions();
156
157 return Promise
158 .all([
159 grantedOptPermissionsPromise,
160 requiredOptPermissions(feature),
161 ])
162 .then(perms => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200163 let missingPerms = {};
164 for (const type of permissionTypes)
165 missingPerms[type] =
166 perms[1][type].filter(p => !perms[0][type].includes(p))
167 return missingPerms;
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100168 })
169 .catch(cause => {
170 throw new Error(
171 'Couldn\'t compute the missing permissions for "' + feature + '",',
172 {cause});
173 });
174}
175
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200176// Returns true if permissions (a chrome.permissions.Permissions object) is
177// empty (that is, if their properties have empty arrays).
178export function isPermissionsObjectEmpty(permissions) {
179 for (const type of permissionTypes) {
180 if ((permissions[type]?.length ?? 0) > 0) return false;
181 }
182 return true;
183}
184
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100185// Deletes optional permissions which are no longer needed by the current
186// set of enabled features (if |removeLeftoverPerms| is set to true), and sets a
187// badge if some needed permissions are missing.
188export function cleanUpOptPermissions(removeLeftoverPerms = true) {
189 return diffPermissions()
190 .then(perms => {
191 let {missingPermissions, leftoverPermissions} = perms;
192
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200193 if (!isPermissionsObjectEmpty(missingPermissions)) {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100194 actionApi.setBadgeBackgroundColor({color: '#B71C1C'});
195 actionApi.setBadgeText({text: '!'});
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100196 actionApi.setTitle({
197 title: chrome.i18n.getMessage('actionbadge_permissions_requested')
198 });
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100199 } else {
200 actionApi.setBadgeText({text: ''});
201 actionApi.setTitle({title: ''});
202 }
203
204 if (removeLeftoverPerms) {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200205 chrome.permissions.remove(leftoverPermissions);
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100206 }
207 })
208 .catch(err => {
209 console.error(
210 'An error ocurred while cleaning optional permissions: ', err);
211 });
212}