blob: f4009ed556af0c3c10147c1405514991efc4303e [file] [log] [blame]
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +01001import {grantedOptPermissions, 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)) {
16 if (!(opt in options)) {
17 ok = false;
18 options[opt] = optMeta['defaultValue'];
19 }
20 }
21
Adrià Vilanova Martínez413cb442021-09-06 00:30:45 +020022 console.debug('[cleanUpOptions] New options', JSON.stringify(options));
Adrià Vilanova Martínez3465e772021-07-11 19:18:41 +020023
24 if (!ok && !dryRun) {
25 chrome.storage.sync.set(options);
26 }
27
28 return options;
29}
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +020030
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010031// This piece of code is used as part of the getOptions computation, and so
32// isn't that useful. It's exported since we sometimes need to finish the
33// computation in a service worker, where we have access to the
34// chrome.permissions API.
35//
36// It accepts as an argument an object |items| with the same structure of the
37// items saved in the sync storage area, and an array |permissionChecksFeatures|
38// of features
39export function disableItemsWithMissingPermissions(
40 items, permissionChecksFeatures) {
41 return grantedOptPermissions().then(grantedPerms => {
42 let permissionChecksPromises = [];
43 for (const f of permissionChecksFeatures)
44 permissionChecksPromises.push(missingPermissions(f, grantedPerms));
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +020045
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010046 Promise.all(permissionChecksPromises).then(missingPerms => {
47 for (let i = 0; i < permissionChecksFeatures.length; i++)
48 if (missingPerms[i].length > 0)
49 items[permissionChecksFeatures[i]] = false;
Adrià Vilanova Martínez413cb442021-09-06 00:30:45 +020050
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010051 return items;
Adrià Vilanova Martínez413cb442021-09-06 00:30:45 +020052 });
53 });
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +020054}
55
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +010056// Returns a promise which returns the values of options |options| which are
57// stored in the sync storage area.
58//
59// |requireOptionalPermissions| will determine whether to check if the required
60// optional permissions have been granted or not to the options which have such
61// requirements. If it is true, features with missing permissions will have
62// their value set to false.
63//
64// When a kill switch is active, affected options always have their value set to
65// false.
66
67// #!if !production
68let timerId = 0;
69let randomId = btoa(Math.random().toString()).substr(10, 5);
70// #!endif
71export function getOptions(options, requireOptionalPermissions = true) {
72 // #!if !production
73 let timeLabel = 'getOptions--' + randomId + '-' + (timerId++);
74 console.time(timeLabel);
75 // #!endif
76 // Once we only target MV3, this can be greatly simplified.
77 return new Promise((resolve, reject) => {
78 if (typeof options === 'string')
79 options = [options, '_forceDisabledFeatures'];
80 else if (Array.isArray(options))
81 options = [...options, '_forceDisabledFeatures'];
82 else if (options !== null)
83 return reject(new Error(
84 'Unexpected |options| parameter of type ' + (typeof options) +
85 ' (expected: string, array, or null).'));
86
87 chrome.storage.sync.get(options, items => {
88 if (chrome.runtime.lastError)
89 return reject(chrome.runtime.lastError);
90
91 // Handle applicable kill switches which force disable features
92 if (items?._forceDisabledFeatures) {
93 for (let feature of items?._forceDisabledFeatures) {
94 items[feature] = false;
95 }
96
97 delete items._forceDisabledFeatures;
98 }
99
100 if (!requireOptionalPermissions) return resolve(items);
101
102 // Check whether some options have missing permissions which would
103 // force disable these features
104 let permissionChecksFeatures = [];
105 for (const [key, value] of Object.entries(items))
106 if ((key in optionsPrototype) && value &&
107 optionsPrototype[key].requiredOptPermissions?.length)
108 permissionChecksFeatures.push(key);
109
110 if (permissionChecksFeatures.length == 0) return resolve(items);
111
112 // If we don't have access to the chrome.permissions API (content
113 // scripts don't have access to it[1]), do the final piece of
114 // computation in the service worker/background script.
115 // [1]: https://developer.chrome.com/docs/extensions/mv3/content_scripts/
116
117 // #!if !production
118 console.debug('We are about to start checking granted permissions');
119 console.timeLog(timeLabel);
120 // #!endif
121 if (!chrome.permissions) {
122 return chrome.runtime.sendMessage(
123 {
124 message: 'runDisableItemsWithMissingPermissions',
125 options: {
126 items,
127 permissionChecksFeatures,
128 },
129 },
130 response => {
131 if (response === undefined)
132 return reject(new Error(
133 'An error ocurred while communicating with the service worker: ' +
134 chrome.runtime.lastError.message));
135
136 if (response.status == 'rejected')
137 return reject(response.error);
138 if (response.status == 'resolved')
139 return resolve(response.items);
140 return reject(new Error(
141 'An unknown response was recieved from service worker.'));
142 });
143 }
144
145 disableItemsWithMissingPermissions(items, permissionChecksFeatures)
146 .then(finalItems => resolve(finalItems))
147 .catch(err => reject(err));
148 });
149 })
150 // #!if !production
151 .then(items => {
152 console.group('getOptions(options); resolved; options: ', options);
153 console.timeEnd(timeLabel);
154 console.groupEnd();
155 return items;
156 })
157 .catch(err => {
158 console.group('getOptions(options); rejected; options: ', options);
159 console.timeEnd(timeLabel);
160 console.groupEnd();
161 throw err;
162 })
163 // #!endif
164 ;
165}
166
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200167// Returns a promise which returns whether the |option| option/feature is
Adrià Vilanova Martínez5120dbb2022-01-04 03:21:17 +0100168// currently enabled. If the feature requires optional permissions to work,
169// |requireOptionalPermissions| will determine whether to check if the required
170// optional permissions have been granted or not.
171export function isOptionEnabled(option, requireOptionalPermissions = true) {
Adrià Vilanova Martínezd269c622021-09-04 18:35:55 +0200172 return getOptions(option).then(options => {
173 return options?.[option] === true;
174 });
175}