no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / browser / themes / BuiltInThemes.sys.mjs
blobc2d5dd7a18895ae8b4afbf386f122e7899c48cda
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
11   BuiltInThemeConfig: "resource:///modules/BuiltInThemeConfig.sys.mjs",
12 });
14 const ColorwayL10n = new Localization(["browser/colorways.ftl"], true);
16 const kActiveThemePref = "extensions.activeThemeID";
17 const kRetainedThemesPref = "browser.theme.retainedExpiredThemes";
19 const ColorwayIntensityIdPostfixToL10nMap = [
20   ["-soft-colorway@mozilla.org", "colorway-intensity-soft"],
21   ["-balanced-colorway@mozilla.org", "colorway-intensity-balanced"],
22   ["-bold-colorway@mozilla.org", "colorway-intensity-bold"],
25 XPCOMUtils.defineLazyPreferenceGetter(
26   lazy,
27   "retainedThemes",
28   kRetainedThemesPref,
29   null,
30   null,
31   val => {
32     if (!val) {
33       return [];
34     }
36     let parsedVal;
37     try {
38       parsedVal = JSON.parse(val);
39     } catch (ex) {
40       console.log(`${kRetainedThemesPref} has invalid value.`);
41       return [];
42     }
44     return parsedVal;
45   }
48 class _BuiltInThemes {
49   /**
50    * The list of themes to be installed. This is exposed on the class so tests
51    * can set custom config files.
52    */
53   builtInThemeMap = lazy.BuiltInThemeConfig;
55   /**
56    * @param {string} id An addon's id string.
57    * @returns {string}
58    *   If `id` refers to a built-in theme, returns a path pointing to the
59    *   theme's preview image. Null otherwise.
60    */
61   previewForBuiltInThemeId(id) {
62     let theme = this.builtInThemeMap.get(id);
63     if (theme) {
64       return `${theme.path}preview.svg`;
65     }
67     return null;
68   }
70   /**
71    * If the active theme is built-in, this function calls
72    * AddonManager.maybeInstallBuiltinAddon for that theme.
73    */
74   maybeInstallActiveBuiltInTheme() {
75     const activeThemeID = Services.prefs.getStringPref(
76       kActiveThemePref,
77       "default-theme@mozilla.org"
78     );
79     let activeBuiltInTheme = this.builtInThemeMap.get(activeThemeID);
81     if (activeBuiltInTheme) {
82       lazy.AddonManager.maybeInstallBuiltinAddon(
83         activeThemeID,
84         activeBuiltInTheme.version,
85         `resource://builtin-themes/${activeBuiltInTheme.path}`
86       );
87     }
88   }
90   /**
91    * Ensures that all built-in themes are installed and expired themes are
92    * uninstalled.
93    */
94   async ensureBuiltInThemes() {
95     let installPromises = [];
96     installPromises.push(this._uninstallExpiredThemes());
98     const now = new Date();
99     for (let [id, themeInfo] of this.builtInThemeMap.entries()) {
100       if (
101         !themeInfo.expiry ||
102         lazy.retainedThemes.includes(id) ||
103         new Date(themeInfo.expiry) > now
104       ) {
105         installPromises.push(
106           lazy.AddonManager.maybeInstallBuiltinAddon(
107             id,
108             themeInfo.version,
109             themeInfo.path
110           )
111         );
112       }
113     }
115     await Promise.all(installPromises);
116   }
118   /**
119    * This looks up the id in a Map rather than accessing a property on
120    * the addon itself. That makes calls to this function O(m) where m is the
121    * total number of built-in themes offered now or in the past. Since we
122    * are using a Map, calls are O(1) in the average case.
123    *
124    * @param {string} id
125    *   A theme's ID.
126    * @returns {boolean}
127    *   Returns true if the theme is expired. False otherwise.
128    */
129   themeIsExpired(id) {
130     let themeInfo = this.builtInThemeMap.get(id);
131     return themeInfo?.expiry && new Date(themeInfo.expiry) < new Date();
132   }
134   /**
135    * @param {string} id
136    *   The theme's id.
137    * @returns {boolean}
138    *   True if the theme with id `id` is both expired and retained. That is,
139    *   the user has the ability to use it after its expiry date.
140    */
141   isRetainedExpiredTheme(id) {
142     return lazy.retainedThemes.includes(id) && this.themeIsExpired(id);
143   }
145   /**
146    * @param {string} id
147    *   The theme's id.
148    * @returns {boolean}
149    *   True if the theme with id `id` is from the currently active theme.
150    */
151   isActiveTheme(id) {
152     return (
153       id ===
154       Services.prefs.getStringPref(
155         kActiveThemePref,
156         "default-theme@mozilla.org"
157       )
158     );
159   }
161   /**
162    * Uninstalls themes after they expire. If the expired theme is active, then
163    * it is not uninstalled. Instead, it is saved so that the user can use it
164    * indefinitely.
165    */
166   async _uninstallExpiredThemes() {
167     const activeThemeID = Services.prefs.getStringPref(
168       kActiveThemePref,
169       "default-theme@mozilla.org"
170     );
171     const now = new Date();
172     const expiredThemes = Array.from(this.builtInThemeMap.entries()).filter(
173       ([id, themeInfo]) =>
174         !!themeInfo.expiry &&
175         !lazy.retainedThemes.includes(id) &&
176         new Date(themeInfo.expiry) <= now
177     );
178     for (let [id] of expiredThemes) {
179       if (id == activeThemeID) {
180         let shouldRetain = true;
182         try {
183           let addon = await lazy.AddonManager.getAddonByID(id);
184           if (addon) {
185             // Only add the id to the retain themes pref if it is
186             // also a built-in themes (and don't if it was migrated
187             // xpi files installed in the user profile).
188             shouldRetain = addon.isBuiltinColorwayTheme;
189           }
190         } catch (e) {
191           console.error(
192             `Failed to retrieve active theme AddonWrapper ${id}`,
193             e
194           );
195         }
197         if (shouldRetain) {
198           this._retainLimitedTimeTheme(id);
199         }
200       } else {
201         try {
202           let addon = await lazy.AddonManager.getAddonByID(id);
203           // Only uninstall the expired colorways theme if they are not
204           // migrated builtins (because on migrated to xpi files
205           // installed in the user profile they are also removed
206           // from the retainedExpiredThemes pref).
207           if (addon?.isBuiltinColorwayTheme) {
208             await addon.uninstall();
209           }
210         } catch (e) {
211           console.error(`Failed to uninstall expired theme ${id}`, e);
212         }
213       }
214     }
215   }
217   /**
218    * Set a pref to ensure that the user can continue to use a specified theme
219    * past its expiry date.
220    *
221    * @param {string} id
222    *   The ID of the theme to retain.
223    */
224   _retainLimitedTimeTheme(id) {
225     if (!lazy.retainedThemes.includes(id)) {
226       lazy.retainedThemes.push(id);
227       Services.prefs.setStringPref(
228         kRetainedThemesPref,
229         JSON.stringify(lazy.retainedThemes)
230       );
231     }
232   }
234   /**
235    * Removes from the retained expired theme list colorways themes that have been
236    * migrated from the one installed in the built-in XPIProvider location
237    * to an AMO hosted xpi installed in the user profile XPIProvider location.
238    *
239    * @param {string} id
240    *   The ID of the theme to remove from the retained themes list.
241    */
243   unretainMigratedColorwayTheme(id) {
244     if (lazy.retainedThemes.includes(id)) {
245       const retainedThemes = lazy.retainedThemes.filter(
246         retainedThemeId => retainedThemeId !== id
247       );
248       Services.prefs.setStringPref(
249         kRetainedThemesPref,
250         JSON.stringify(retainedThemes)
251       );
252     }
253   }
255   /**
256    * Colorway collections are usually divided into and presented as "groups".
257    * A group either contains closely related colorways, e.g. stemming from the
258    * same base color but with different intensities (soft, balanced, and bold),
259    * or if the current collection doesn't have intensities, each colorway is
260    * their own group. Group name localization is optional.
261    *
262    * @param {string} colorwayId
263    *   The ID of the colorway add-on.
264    * @returns {string}
265    *   Localized colorway group name. null if there's no such name, in which
266    *   case the caller should fall back on getting a name from the add-on API.
267    */
268   getLocalizedColorwayGroupName(colorwayId) {
269     return this._getColorwayString(colorwayId, "groupName");
270   }
272   /**
273    * @param {string} colorwayId
274    *   The ID of the colorway add-on.
275    * @returns {string}
276    *   L10nId for intensity value of the colorway with the provided id, null if
277    *   there's none.
278    */
279   getColorwayIntensityL10nId(colorwayId) {
280     const result = ColorwayIntensityIdPostfixToL10nMap.find(
281       ([postfix, l10nId]) => colorwayId.endsWith(postfix)
282     );
283     return result ? result[1] : null;
284   }
286   /**
287    * @param {string} colorwayId
288    *   The ID of the colorway add-on.
289    * @returns {string}
290    *   Localized description of the colorway with the provided id, null if
291    *   there's none.
292    */
293   getLocalizedColorwayDescription(colorwayId) {
294     return this._getColorwayString(colorwayId, "description");
295   }
297   _getColorwayString(colorwayId, stringType) {
298     let l10nId = this.builtInThemeMap.get(colorwayId)?.l10nId?.[stringType];
299     let s;
300     if (l10nId) {
301       [s] = ColorwayL10n.formatMessagesSync([
302         {
303           id: l10nId,
304         },
305       ]);
306     }
307     return s?.value || null;
308   }
311 export var BuiltInThemes = new _BuiltInThemes();