blob: 75146432faeb01e16de57d0e31d90d2624eab2b9 [file] [log] [blame]
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +02001import {grantedOptPermissions, isPermissionsObjectEmpty, missingPermissions} from './optionsPermissions.js';
Adrià Vilanova Martínezb523be92024-05-25 19:14:19 +02002import {optionsPrototype} from './optionsPrototype';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02003import specialOptions from './specialOptions.json5';
4
5export {optionsPrototype, specialOptions};
6
7// Adds missing options with their default value. If |dryRun| is set to false,
8// they are also saved to the sync storage area.
9export function cleanUpOptions(options, dryRun = false) {
Adrià Vilanova Martínez413cb442021-09-06 00:30:45 +020010 console.debug('[cleanUpOptions] Previous options', JSON.stringify(options));
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020011
12 if (typeof options !== 'object' || options === null) options = {};
13
14 var ok = true;
15 for (const [opt, optMeta] of Object.entries(optionsPrototype)) {
Adrià Vilanova Martínezd7b2b362023-07-23 01:50:42 +020016 if (!(opt in options) &&
17 optMeta['killSwitchType'] !== 'internalKillSwitch') {
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020018 ok = false;
19 options[opt] = optMeta['defaultValue'];
20 }
21 }
22
Adrià Vilanova Martínez413cb442021-09-06 00:30:45 +020023 console.debug('[cleanUpOptions] New options', JSON.stringify(options));
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020024
25 if (!ok && !dryRun) {
26 chrome.storage.sync.set(options);
27 }
28
29 return options;
30}
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +020031
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010032// This piece of code is used as part of the getOptions computation, and so
33// isn't that useful. It's exported since we sometimes need to finish the
34// computation in a service worker, where we have access to the
35// chrome.permissions API.
36//
37// It accepts as an argument an object |items| with the same structure of the
38// items saved in the sync storage area, and an array |permissionChecksFeatures|
39// of features
40export function disableItemsWithMissingPermissions(
41 items, permissionChecksFeatures) {
42 return grantedOptPermissions().then(grantedPerms => {
43 let permissionChecksPromises = [];
44 for (const f of permissionChecksFeatures)
45 permissionChecksPromises.push(missingPermissions(f, grantedPerms));
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +020046
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010047 Promise.all(permissionChecksPromises).then(missingPerms => {
48 for (let i = 0; i < permissionChecksFeatures.length; i++)
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +020049 if (!isPermissionsObjectEmpty(missingPerms[i]))
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010050 items[permissionChecksFeatures[i]] = false;
Adrià Vilanova Martínez413cb442021-09-06 00:30:45 +020051
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010052 return items;
Adrià Vilanova Martínez413cb442021-09-06 00:30:45 +020053 });
54 });
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +020055}
56
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010057// Returns a promise which returns the values of options |options| which are
58// stored in the sync storage area.
59//
60// |requireOptionalPermissions| will determine whether to check if the required
61// optional permissions have been granted or not to the options which have such
62// requirements. If it is true, features with missing permissions will have
63// their value set to false.
64//
65// When a kill switch is active, affected options always have their value set to
66// false.
67
68// #!if !production
69let timerId = 0;
70let randomId = btoa(Math.random().toString()).substr(10, 5);
71// #!endif
72export function getOptions(options, requireOptionalPermissions = true) {
73 // #!if !production
Adrià Vilanova Martínez82ad3bc2024-12-05 18:35:33 +010074 const timeLabel = 'getOptions--' + randomId + '-' + (timerId++);
75 const startMark = `mark_start_get_options_${timeLabel}`;
76 const grantedPermissionsCheckMark =
77 `mark_get_options_check_granted_permissions_${timeLabel}`;
78 const endMark = `mark_end_get_options_${timeLabel}`;
79 const measureName = `measure_get_options_${timeLabel}`;
80 window.performance.mark(startMark, {detail: {options}});
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010081 // #!endif
82 // Once we only target MV3, this can be greatly simplified.
83 return new Promise((resolve, reject) => {
84 if (typeof options === 'string')
85 options = [options, '_forceDisabledFeatures'];
86 else if (Array.isArray(options))
87 options = [...options, '_forceDisabledFeatures'];
88 else if (options !== null)
89 return reject(new Error(
90 'Unexpected |options| parameter of type ' + (typeof options) +
91 ' (expected: string, array, or null).'));
92
93 chrome.storage.sync.get(options, items => {
94 if (chrome.runtime.lastError)
95 return reject(chrome.runtime.lastError);
96
97 // Handle applicable kill switches which force disable features
98 if (items?._forceDisabledFeatures) {
Adrià Vilanova Martínez92323882024-05-18 20:56:19 +020099 for (let feature of items._forceDisabledFeatures) {
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100100 items[feature] = false;
101 }
102
103 delete items._forceDisabledFeatures;
104 }
105
106 if (!requireOptionalPermissions) return resolve(items);
107
108 // Check whether some options have missing permissions which would
109 // force disable these features
110 let permissionChecksFeatures = [];
111 for (const [key, value] of Object.entries(items))
112 if ((key in optionsPrototype) && value &&
113 optionsPrototype[key].requiredOptPermissions?.length)
114 permissionChecksFeatures.push(key);
115
116 if (permissionChecksFeatures.length == 0) return resolve(items);
117
118 // If we don't have access to the chrome.permissions API (content
119 // scripts don't have access to it[1]), do the final piece of
120 // computation in the service worker/background script.
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200121 // [1]:
122 // https://developer.chrome.com/docs/extensions/mv3/content_scripts/
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100123
124 // #!if !production
Adrià Vilanova Martínez82ad3bc2024-12-05 18:35:33 +0100125 window.performance.mark(
126 grantedPermissionsCheckMark, {detail: {options}});
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100127 // #!endif
128 if (!chrome.permissions) {
129 return chrome.runtime.sendMessage(
130 {
131 message: 'runDisableItemsWithMissingPermissions',
132 options: {
133 items,
134 permissionChecksFeatures,
135 },
136 },
137 response => {
138 if (response === undefined)
139 return reject(new Error(
140 'An error ocurred while communicating with the service worker: ' +
141 chrome.runtime.lastError.message));
142
143 if (response.status == 'rejected')
144 return reject(response.error);
145 if (response.status == 'resolved')
146 return resolve(response.items);
147 return reject(new Error(
148 'An unknown response was recieved from service worker.'));
149 });
150 }
151
152 disableItemsWithMissingPermissions(items, permissionChecksFeatures)
153 .then(finalItems => resolve(finalItems))
154 .catch(err => reject(err));
155 });
156 })
157 // #!if !production
158 .then(items => {
Adrià Vilanova Martínez82ad3bc2024-12-05 18:35:33 +0100159 window.performance.mark(endMark, {detail: {options}});
160 window.performance.measure(measureName, {
161 detail: {options},
162 start: startMark,
163 end: endMark,
164 });
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100165 return items;
166 })
167 .catch(err => {
Adrià Vilanova Martínez82ad3bc2024-12-05 18:35:33 +0100168 window.performance.mark(endMark, {detail: {options}});
169 window.performance.measure(measureName, {
170 detail: {options},
171 start: startMark,
172 end: endMark,
173 });
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100174 throw err;
175 })
176 // #!endif
177 ;
178}
179
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200180// Returns a promise which returns whether the |option| option/feature is
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100181// currently enabled. If the feature requires optional permissions to work,
182// |requireOptionalPermissions| will determine whether to check if the required
183// optional permissions have been granted or not.
184export function isOptionEnabled(option, requireOptionalPermissions = true) {
Adrià Vilanova Martínez92323882024-05-18 20:56:19 +0200185 return getOptions(option, requireOptionalPermissions).then(options => {
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200186 return options?.[option] === true;
187 });
188}
Adrià Vilanova Martínez994d1e22023-07-23 01:52:15 +0200189
190export function getForceDisabledFeatures() {
191 return new Promise((res, rej) => {
192 chrome.storage.sync.get('_forceDisabledFeatures', items => {
193 if (chrome.runtime.lastError) return rej(chrome.runtime.lastError);
194 res(items?.['_forceDisabledFeatures'] ?? []);
195 });
196 });
197}