blob: 162fd9320103f658dfade95b132b5abf1acfc483 [file] [log] [blame]
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +02001import {grantedOptPermissions, isPermissionsObjectEmpty, missingPermissions} from './optionsPermissions.js';
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +02002import optionsPrototype from './optionsPrototype.json5';
3import 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
74 let timeLabel = 'getOptions--' + randomId + '-' + (timerId++);
75 console.time(timeLabel);
76 // #!endif
77 // Once we only target MV3, this can be greatly simplified.
78 return new Promise((resolve, reject) => {
79 if (typeof options === 'string')
80 options = [options, '_forceDisabledFeatures'];
81 else if (Array.isArray(options))
82 options = [...options, '_forceDisabledFeatures'];
83 else if (options !== null)
84 return reject(new Error(
85 'Unexpected |options| parameter of type ' + (typeof options) +
86 ' (expected: string, array, or null).'));
87
88 chrome.storage.sync.get(options, items => {
89 if (chrome.runtime.lastError)
90 return reject(chrome.runtime.lastError);
91
92 // Handle applicable kill switches which force disable features
93 if (items?._forceDisabledFeatures) {
94 for (let feature of items?._forceDisabledFeatures) {
95 items[feature] = false;
96 }
97
98 delete items._forceDisabledFeatures;
99 }
100
101 if (!requireOptionalPermissions) return resolve(items);
102
103 // Check whether some options have missing permissions which would
104 // force disable these features
105 let permissionChecksFeatures = [];
106 for (const [key, value] of Object.entries(items))
107 if ((key in optionsPrototype) && value &&
108 optionsPrototype[key].requiredOptPermissions?.length)
109 permissionChecksFeatures.push(key);
110
111 if (permissionChecksFeatures.length == 0) return resolve(items);
112
113 // If we don't have access to the chrome.permissions API (content
114 // scripts don't have access to it[1]), do the final piece of
115 // computation in the service worker/background script.
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200116 // [1]:
117 // https://developer.chrome.com/docs/extensions/mv3/content_scripts/
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100118
119 // #!if !production
Adrià Vilanova Martínez310c2902022-07-04 00:31:41 +0200120 console.debug(
121 'We are about to start checking granted permissions');
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100122 console.timeLog(timeLabel);
123 // #!endif
124 if (!chrome.permissions) {
125 return chrome.runtime.sendMessage(
126 {
127 message: 'runDisableItemsWithMissingPermissions',
128 options: {
129 items,
130 permissionChecksFeatures,
131 },
132 },
133 response => {
134 if (response === undefined)
135 return reject(new Error(
136 'An error ocurred while communicating with the service worker: ' +
137 chrome.runtime.lastError.message));
138
139 if (response.status == 'rejected')
140 return reject(response.error);
141 if (response.status == 'resolved')
142 return resolve(response.items);
143 return reject(new Error(
144 'An unknown response was recieved from service worker.'));
145 });
146 }
147
148 disableItemsWithMissingPermissions(items, permissionChecksFeatures)
149 .then(finalItems => resolve(finalItems))
150 .catch(err => reject(err));
151 });
152 })
153 // #!if !production
154 .then(items => {
155 console.group('getOptions(options); resolved; options: ', options);
156 console.timeEnd(timeLabel);
157 console.groupEnd();
158 return items;
159 })
160 .catch(err => {
161 console.group('getOptions(options); rejected; options: ', options);
162 console.timeEnd(timeLabel);
163 console.groupEnd();
164 throw err;
165 })
166 // #!endif
167 ;
168}
169
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200170// Returns a promise which returns whether the |option| option/feature is
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100171// currently enabled. If the feature requires optional permissions to work,
172// |requireOptionalPermissions| will determine whether to check if the required
173// optional permissions have been granted or not.
174export function isOptionEnabled(option, requireOptionalPermissions = true) {
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200175 return getOptions(option).then(options => {
176 return options?.[option] === true;
177 });
178}
Adrià Vilanova Martínez994d1e22023-07-23 01:52:15 +0200179
180export function getForceDisabledFeatures() {
181 return new Promise((res, rej) => {
182 chrome.storage.sync.get('_forceDisabledFeatures', items => {
183 if (chrome.runtime.lastError) return rej(chrome.runtime.lastError);
184 res(items?.['_forceDisabledFeatures'] ?? []);
185 });
186 });
187}