blob: a475ac7c9a8a41578967a16ced88d3819e168800 [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
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/*',
27 'https://support.google.com/*/profile/*',
28 'https://support.google.com/profile/*',
29 ]),
30};
31
32const permissionTypes = ['origins', 'permissions'];
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010033
34// Returns an array of optional permissions needed by |feature|.
35export function requiredOptPermissions(feature) {
36 if (!(feature in optionsPrototype)) {
37 console.error('"' + feature + '" feature doesn\'t exist.');
38 return [];
39 }
40
41 return optionsPrototype[feature]?.requiredOptPermissions ?? [];
42}
43
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020044// Returns a promise resolving to the optional permissions needed by all the
45// current enabled features.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010046export function currentRequiredOptPermissions() {
47 return getOptions(null, /* requireOptionalPermissions = */ false)
48 .then(options => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020049 let permissions = {
50 origins: [],
51 permissions: [],
52 };
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010053
54 // For each option
55 for (const [opt, optMeta] of Object.entries(optionsPrototype))
56 // If the option is enabled
57 if (options[opt])
58 // Add its required optional permissions to the list
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020059 for (const type of permissionTypes)
60 permissions[type].push(
61 ...(optMeta.requiredOptPermissions?.[type] ?? []));
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010062
63 return permissions;
64 });
65}
66
67// Ensures that all the optional permissions required by |feature| are granted,
68// and requests them otherwise. It returns a promise which resolves specifying
69// whether the permissions were granted or not.
70export function ensureOptPermissions(feature) {
71 return new Promise((resolve, reject) => {
72 let permissions = requiredOptPermissions(feature);
73
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020074 chrome.permissions.contains(permissions, isAlreadyGranted => {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010075 if (isAlreadyGranted) return resolve(true);
76
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020077 chrome.permissions.request(permissions, granted => {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010078 // If there was an error, reject the promise.
79 if (granted === undefined)
80 return reject(new Error(
81 chrome.runtime.lastError.message ??
82 'An unknown error occurred while requesting the permisisons'));
83
84 // If the permission is granted we should maybe remove the warning
85 // badge.
86 if (granted) cleanUpOptPermissions(/* removeLeftoverPerms = */ false);
87
88 return resolve(granted);
89 });
90 });
91 });
92}
93
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020094// Returns a promise resolving to the currently granted optional permissions
95// (i.e. excluding required permissions).
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010096export function grantedOptPermissions() {
97 return new Promise((resolve, reject) => {
98 chrome.permissions.getAll(response => {
99 if (response === undefined)
100 return reject(new Error(
101 chrome.runtime.lastError.message ??
102 'An unknown error occurred while calling chrome.permissions.getAll()'));
103
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200104 let optPermissions = {};
105 for (const type of permissionTypes)
106 optPermissions[type] =
107 response[type].filter(p => !requiredPermissions[type].has(p));
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100108 resolve(optPermissions);
109 });
110 });
111}
112
113// Returns a promise resolving to an object with 2 properties:
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200114// - missingPermissions: optional permissions which are required by enabled
115// features and haven't been granted yet.
116// - leftoverPermissions: optional permissions which are granted but are no
117// longer needed.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100118export function diffPermissions() {
119 return Promise
120 .all([
121 grantedOptPermissions(),
122 currentRequiredOptPermissions(),
123 ])
124 .then(perms => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200125 let diff = {
126 missingPermissions: {},
127 leftoverPermissions: {},
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100128 };
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200129 for (const type of permissionTypes) {
130 diff.missingPermissions[type] =
131 perms[1][type].filter(p => !perms[0][type].includes(p));
132 diff.leftoverPermissions[type] =
133 perms[0][type].filter(p => !perms[1][type].includes(p));
134 }
135 return diff;
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100136 })
137 .catch(cause => {
138 throw new Error(
139 'Couldn\'t compute the missing and leftover permissions.', {cause});
140 });
141}
142
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200143// Returns a promise which resolves to the required optional permissions of
144// |feature| which are missing.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100145//
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200146// Accepts an argument |grantedPermissions| with the granted permissions,
147// otherwise the function will call grantedOptPermissions() to retrieve them.
148// This can be used to prevent calling chrome.permissions.getAll() repeteadly.
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100149export function missingPermissions(feature, grantedPermissions = null) {
150 let grantedOptPermissionsPromise;
151 if (grantedPermissions !== null)
152 grantedOptPermissionsPromise = new Promise((res, rej) => {
153 res(grantedPermissions);
154 });
155 else
156 grantedOptPermissionsPromise = grantedOptPermissions();
157
158 return Promise
159 .all([
160 grantedOptPermissionsPromise,
161 requiredOptPermissions(feature),
162 ])
163 .then(perms => {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200164 let missingPerms = {};
165 for (const type of permissionTypes)
166 missingPerms[type] =
167 perms[1][type].filter(p => !perms[0][type].includes(p))
168 return missingPerms;
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100169 })
170 .catch(cause => {
171 throw new Error(
172 'Couldn\'t compute the missing permissions for "' + feature + '",',
173 {cause});
174 });
175}
176
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200177// Returns true if permissions (a chrome.permissions.Permissions object) is
178// empty (that is, if their properties have empty arrays).
179export function isPermissionsObjectEmpty(permissions) {
180 for (const type of permissionTypes) {
181 if ((permissions[type]?.length ?? 0) > 0) return false;
182 }
183 return true;
184}
185
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100186// Deletes optional permissions which are no longer needed by the current
187// set of enabled features (if |removeLeftoverPerms| is set to true), and sets a
188// badge if some needed permissions are missing.
189export function cleanUpOptPermissions(removeLeftoverPerms = true) {
190 return diffPermissions()
191 .then(perms => {
192 let {missingPermissions, leftoverPermissions} = perms;
193
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200194 if (!isPermissionsObjectEmpty(missingPermissions)) {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100195 actionApi.setBadgeBackgroundColor({color: '#B71C1C'});
196 actionApi.setBadgeText({text: '!'});
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100197 actionApi.setTitle({
198 title: chrome.i18n.getMessage('actionbadge_permissions_requested')
199 });
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100200 } else {
201 actionApi.setBadgeText({text: ''});
202 actionApi.setTitle({title: ''});
203 }
204
205 if (removeLeftoverPerms) {
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200206 chrome.permissions.remove(leftoverPermissions);
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100207 }
208 })
209 .catch(err => {
210 console.error(
211 'An error ocurred while cleaning optional permissions: ', err);
212 });
213}