no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / browser / modules / HomePage.sys.mjs
blob612e559e2c2f705bc4d3591afe973f0edc031e8b
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
9   ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
10   ExtensionPreferencesManager:
11     "resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
12   IgnoreLists: "resource://gre/modules/IgnoreLists.sys.mjs",
13   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
14 });
16 const kPrefName = "browser.startup.homepage";
17 const kDefaultHomePage = "about:home";
18 const kExtensionControllerPref =
19   "browser.startup.homepage_override.extensionControlled";
20 const kHomePageIgnoreListId = "homepage-urls";
21 const kWidgetId = "home-button";
22 const kWidgetRemovedPref = "browser.engagement.home-button.has-removed";
24 function getHomepagePref(useDefault) {
25   let homePage;
26   let prefs = Services.prefs;
27   if (useDefault) {
28     prefs = prefs.getDefaultBranch(null);
29   }
30   try {
31     // Historically, this was a localizable pref, but default Firefox builds
32     // don't use this.
33     // Distributions and local customizations might still use this, so let's
34     // keep it.
35     homePage = prefs.getComplexValue(kPrefName, Ci.nsIPrefLocalizedString).data;
36   } catch (ex) {}
38   if (!homePage) {
39     homePage = prefs.getStringPref(kPrefName);
40   }
42   // Apparently at some point users ended up with blank home pages somehow.
43   // If that happens, reset the pref and read it again.
44   if (!homePage && !useDefault) {
45     Services.prefs.clearUserPref(kPrefName);
46     homePage = getHomepagePref(true);
47   }
49   return homePage;
52 /**
53  * HomePage provides tools to keep track of the current homepage, and the
54  * applications's default homepage. It includes tools to insure that certain
55  * urls are ignored. As a result, all set/get requests for the homepage
56  * preferences should be routed through here.
57  */
58 export let HomePage = {
59   // This is an array of strings that should be matched against URLs to see
60   // if they should be ignored or not.
61   _ignoreList: [],
63   // A promise that is set when initialization starts and resolved when it
64   // completes.
65   _initializationPromise: null,
67   /**
68    * Used to initialise the ignore lists. This may be called later than
69    * the first call to get or set, which may cause a used to get an ignored
70    * homepage, but this is deemed acceptable, as we'll correct it once
71    * initialised.
72    */
73   async delayedStartup() {
74     if (this._initializationPromise) {
75       await this._initializationPromise;
76       return;
77     }
79     Services.telemetry.setEventRecordingEnabled("homepage", true);
81     // Now we have the values, listen for future updates.
82     this._ignoreListListener = this._handleIgnoreListUpdated.bind(this);
84     this._initializationPromise = lazy.IgnoreLists.getAndSubscribe(
85       this._ignoreListListener
86     );
88     this._addCustomizableUiListener();
90     const current = await this._initializationPromise;
92     await this._handleIgnoreListUpdated({ data: { current } });
93   },
95   /**
96    * Gets the homepage for the given window.
97    *
98    * @param {DOMWindow} [aWindow]
99    *   The window associated with the get, used to check for private browsing
100    *   mode. If not supplied, normal mode is assumed.
101    * @returns {string}
102    *   Returns the home page value, this could be a single url, or a `|`
103    *   separated list of URLs.
104    */
105   get(aWindow) {
106     let homePages = getHomepagePref();
107     if (
108       lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
109       (aWindow && lazy.PrivateBrowsingUtils.isWindowPrivate(aWindow))
110     ) {
111       // If an extension controls the setting and does not have private
112       // browsing permission, use the default setting.
113       let extensionControlled = Services.prefs.getBoolPref(
114         kExtensionControllerPref,
115         false
116       );
117       let privateAllowed = Services.prefs.getBoolPref(
118         "browser.startup.homepage_override.privateAllowed",
119         false
120       );
121       // There is a potential on upgrade that the prefs are not set yet, so we double check
122       // for moz-extension.
123       if (
124         !privateAllowed &&
125         (extensionControlled || homePages.includes("moz-extension://"))
126       ) {
127         return this.getDefault();
128       }
129     }
131     if (homePages == "about:blank") {
132       homePages = "chrome://browser/content/blanktab.html";
133     }
135     return homePages;
136   },
138   /**
139    * @returns {string}
140    *   Returns the application default homepage.
141    */
142   getDefault() {
143     return getHomepagePref(true);
144   },
146   /**
147    * @returns {string}
148    *   Returns the original application homepage URL (not from prefs).
149    */
150   getOriginalDefault() {
151     return kDefaultHomePage;
152   },
154   /**
155    * @returns {boolean}
156    *   Returns true if the homepage has been changed.
157    */
158   get overridden() {
159     return Services.prefs.prefHasUserValue(kPrefName);
160   },
162   /**
163    * @returns {boolean}
164    *   Returns true if the homepage preference is locked.
165    */
166   get locked() {
167     return Services.prefs.prefIsLocked(kPrefName);
168   },
170   /**
171    * @returns {boolean}
172    *   Returns true if the current homepage is the application default.
173    */
174   get isDefault() {
175     return HomePage.get() === kDefaultHomePage;
176   },
178   /**
179    * Sets the homepage preference to a new page.
180    *
181    * @param {string} value
182    *   The new value to set the preference to. This could be a single url, or a
183    *   `|` separated list of URLs.
184    */
185   async set(value) {
186     await this.delayedStartup();
188     if (await this.shouldIgnore(value)) {
189       console.error(
190         `Ignoring homepage setting for ${value} as it is on the ignore list.`
191       );
192       Services.telemetry.recordEvent(
193         "homepage",
194         "preference",
195         "ignore",
196         "set_blocked"
197       );
198       return false;
199     }
200     Services.prefs.setStringPref(kPrefName, value);
201     this._maybeAddHomeButtonToToolbar(value);
202     return true;
203   },
205   /**
206    * Sets the homepage preference to a new page. This is an synchronous version
207    * that should only be used when we know the source is safe as it bypasses the
208    * ignore list, e.g. when setting directly to about:blank or a value not
209    * supplied externally.
210    *
211    * @param {string} value
212    *   The new value to set the preference to. This could be a single url, or a
213    *   `|` separated list of URLs.
214    */
215   safeSet(value) {
216     Services.prefs.setStringPref(kPrefName, value);
217   },
219   /**
220    * Clears the homepage preference if it is not the default. Note that for
221    * policy/locking use, the default homepage might not be about:home after this.
222    */
223   clear() {
224     Services.prefs.clearUserPref(kPrefName);
225   },
227   /**
228    * Resets the homepage preference to be about:home.
229    */
230   reset() {
231     Services.prefs.setStringPref(kPrefName, kDefaultHomePage);
232   },
234   /**
235    * Determines if a url should be ignored according to the ignore list.
236    *
237    * @param {string} url
238    *   A string that is the url or urls to be ignored.
239    * @returns {boolean}
240    *   True if the url should be ignored.
241    */
242   async shouldIgnore(url) {
243     await this.delayedStartup();
245     const lowerURL = url.toLowerCase();
246     return this._ignoreList.some(code => lowerURL.includes(code.toLowerCase()));
247   },
249   /**
250    * Handles updates of the ignore list, checking the existing preference and
251    * correcting it as necessary.
252    *
253    * @param {Object} eventData
254    *   The event data as received from RemoteSettings.
255    */
256   async _handleIgnoreListUpdated({ data: { current } }) {
257     for (const entry of current) {
258       if (entry.id == kHomePageIgnoreListId) {
259         this._ignoreList = [...entry.matches];
260       }
261     }
263     // Only check if we're overridden as we assume the default value is fine,
264     // or won't be changeable (e.g. enterprise policy).
265     if (this.overridden) {
266       let homePages = getHomepagePref().toLowerCase();
267       if (
268         this._ignoreList.some(code => homePages.includes(code.toLowerCase()))
269       ) {
270         if (Services.prefs.getBoolPref(kExtensionControllerPref, false)) {
271           if (Services.appinfo.inSafeMode) {
272             // Add-ons don't get started in safe mode, so just abort this.
273             // We'll get to remove them when we next start in normal mode.
274             return;
275           }
276           // getSetting does not need the module to be loaded.
277           const item = await lazy.ExtensionPreferencesManager.getSetting(
278             "homepage_override"
279           );
280           if (item && item.id) {
281             // During startup some modules may not be loaded yet, so we load
282             // the setting we need prior to removal.
283             await lazy.ExtensionParent.apiManager.asyncLoadModule(
284               "chrome_settings_overrides"
285             );
286             lazy.ExtensionPreferencesManager.removeSetting(
287               item.id,
288               "homepage_override"
289             ).catch(console.error);
290           } else {
291             // If we don't have a setting for it, we assume the pref has
292             // been incorrectly set somehow.
293             Services.prefs.clearUserPref(kExtensionControllerPref);
294             Services.prefs.clearUserPref(
295               "browser.startup.homepage_override.privateAllowed"
296             );
297           }
298         } else {
299           this.clear();
300         }
301         Services.telemetry.recordEvent(
302           "homepage",
303           "preference",
304           "ignore",
305           "saved_reset"
306         );
307       }
308     }
309   },
311   onWidgetRemoved(widgetId) {
312     if (widgetId == kWidgetId) {
313       Services.prefs.setBoolPref(kWidgetRemovedPref, true);
314       lazy.CustomizableUI.removeListener(this);
315     }
316   },
318   /**
319    * Add the home button to the toolbar if the user just set a custom homepage.
320    *
321    * This should only be done once, so we check HOME_BUTTON_REMOVED_PREF which
322    * gets set to true when the home button is removed from the toolbar.
323    *
324    * If the home button is already on the toolbar it won't be moved.
325    */
326   _maybeAddHomeButtonToToolbar(homePage) {
327     if (
328       homePage !== "about:home" &&
329       homePage !== "about:blank" &&
330       !Services.prefs.getBoolPref(kExtensionControllerPref, false) &&
331       !Services.prefs.getBoolPref(kWidgetRemovedPref, false) &&
332       !lazy.CustomizableUI.getWidget(kWidgetId).areaType
333     ) {
334       // Find a spot for the home button, ideally it will be in its default
335       // position beside the stop/refresh button.
336       // Work backwards from the URL bar since it can't be removed and put
337       // the button after the first non-spring we find.
338       let navbarPlacements = lazy.CustomizableUI.getWidgetIdsInArea("nav-bar");
339       let position = navbarPlacements.indexOf("urlbar-container");
340       for (let i = position - 1; i >= 0; i--) {
341         if (!navbarPlacements[i].startsWith("customizableui-special-spring")) {
342           position = i + 1;
343           break;
344         }
345       }
346       lazy.CustomizableUI.addWidgetToArea(kWidgetId, "nav-bar", position);
347     }
348   },
350   _addCustomizableUiListener() {
351     if (!Services.prefs.getBoolPref(kWidgetRemovedPref, false)) {
352       lazy.CustomizableUI.addListener(this);
353     }
354   },