/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var EXPORTED_SYMBOLS = ["HomePage"]; const { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { IgnoreLists: "resource://gre/modules/IgnoreLists.sys.mjs", PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", }); XPCOMUtils.defineLazyModuleGetters(lazy, { CustomizableUI: "resource:///modules/CustomizableUI.jsm", ExtensionParent: "resource://gre/modules/ExtensionParent.jsm", ExtensionPreferencesManager: "resource://gre/modules/ExtensionPreferencesManager.jsm", }); const kPrefName = "browser.startup.homepage"; const kDefaultHomePage = "about:home"; const kExtensionControllerPref = "browser.startup.homepage_override.extensionControlled"; const kHomePageIgnoreListId = "homepage-urls"; const kWidgetId = "home-button"; const kWidgetRemovedPref = "browser.engagement.home-button.has-removed"; function getHomepagePref(useDefault) { let homePage; let prefs = Services.prefs; if (useDefault) { prefs = prefs.getDefaultBranch(null); } try { // Historically, this was a localizable pref, but default Firefox builds // don't use this. // Distributions and local customizations might still use this, so let's // keep it. homePage = prefs.getComplexValue(kPrefName, Ci.nsIPrefLocalizedString).data; } catch (ex) {} if (!homePage) { homePage = prefs.getStringPref(kPrefName); } // Apparently at some point users ended up with blank home pages somehow. // If that happens, reset the pref and read it again. if (!homePage && !useDefault) { Services.prefs.clearUserPref(kPrefName); homePage = getHomepagePref(true); } return homePage; } /** * HomePage provides tools to keep track of the current homepage, and the * applications's default homepage. It includes tools to insure that certain * urls are ignored. As a result, all set/get requests for the homepage * preferences should be routed through here. */ let HomePage = { // This is an array of strings that should be matched against URLs to see // if they should be ignored or not. _ignoreList: [], // A promise that is set when initialization starts and resolved when it // completes. _initializationPromise: null, /** * Used to initialise the ignore lists. This may be called later than * the first call to get or set, which may cause a used to get an ignored * homepage, but this is deemed acceptable, as we'll correct it once * initialised. */ async delayedStartup() { if (this._initializationPromise) { await this._initializationPromise; return; } Services.telemetry.setEventRecordingEnabled("homepage", true); // Now we have the values, listen for future updates. this._ignoreListListener = this._handleIgnoreListUpdated.bind(this); this._initializationPromise = lazy.IgnoreLists.getAndSubscribe( this._ignoreListListener ); this._addCustomizableUiListener(); const current = await this._initializationPromise; await this._handleIgnoreListUpdated({ data: { current } }); }, /** * Gets the homepage for the given window. * * @param {DOMWindow} [aWindow] * The window associated with the get, used to check for private browsing * mode. If not supplied, normal mode is assumed. * @returns {string} * Returns the home page value, this could be a single url, or a `|` * separated list of URLs. */ get(aWindow) { let homePages = getHomepagePref(); if ( lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || (aWindow && lazy.PrivateBrowsingUtils.isWindowPrivate(aWindow)) ) { // If an extension controls the setting and does not have private // browsing permission, use the default setting. let extensionControlled = Services.prefs.getBoolPref( kExtensionControllerPref, false ); let privateAllowed = Services.prefs.getBoolPref( "browser.startup.homepage_override.privateAllowed", false ); // There is a potential on upgrade that the prefs are not set yet, so we double check // for moz-extension. if ( !privateAllowed && (extensionControlled || homePages.includes("moz-extension://")) ) { return this.getDefault(); } } if (homePages == "about:blank") { homePages = "chrome://browser/content/blanktab.html"; } return homePages; }, /** * @returns {string} * Returns the application default homepage. */ getDefault() { return getHomepagePref(true); }, /** * @returns {string} * Returns the original application homepage URL (not from prefs). */ getOriginalDefault() { return kDefaultHomePage; }, /** * @returns {boolean} * Returns true if the homepage has been changed. */ get overridden() { return Services.prefs.prefHasUserValue(kPrefName); }, /** * @returns {boolean} * Returns true if the homepage preference is locked. */ get locked() { return Services.prefs.prefIsLocked(kPrefName); }, /** * @returns {boolean} * Returns true if the current homepage is the application default. */ get isDefault() { return HomePage.get() === kDefaultHomePage; }, /** * Sets the homepage preference to a new page. * * @param {string} value * The new value to set the preference to. This could be a single url, or a * `|` separated list of URLs. */ async set(value) { await this.delayedStartup(); if (await this.shouldIgnore(value)) { console.error( `Ignoring homepage setting for ${value} as it is on the ignore list.` ); Services.telemetry.recordEvent( "homepage", "preference", "ignore", "set_blocked" ); return false; } Services.prefs.setStringPref(kPrefName, value); this._maybeAddHomeButtonToToolbar(value); return true; }, /** * Sets the homepage preference to a new page. This is an synchronous version * that should only be used when we know the source is safe as it bypasses the * ignore list, e.g. when setting directly to about:blank or a value not * supplied externally. * * @param {string} value * The new value to set the preference to. This could be a single url, or a * `|` separated list of URLs. */ safeSet(value) { Services.prefs.setStringPref(kPrefName, value); }, /** * Clears the homepage preference if it is not the default. Note that for * policy/locking use, the default homepage might not be about:home after this. */ clear() { Services.prefs.clearUserPref(kPrefName); }, /** * Resets the homepage preference to be about:home. */ reset() { Services.prefs.setStringPref(kPrefName, kDefaultHomePage); }, /** * Determines if a url should be ignored according to the ignore list. * * @param {string} url * A string that is the url or urls to be ignored. * @returns {boolean} * True if the url should be ignored. */ async shouldIgnore(url) { await this.delayedStartup(); const lowerURL = url.toLowerCase(); return this._ignoreList.some(code => lowerURL.includes(code.toLowerCase())); }, /** * Handles updates of the ignore list, checking the existing preference and * correcting it as necessary. * * @param {Object} eventData * The event data as received from RemoteSettings. */ async _handleIgnoreListUpdated({ data: { current } }) { for (const entry of current) { if (entry.id == kHomePageIgnoreListId) { this._ignoreList = [...entry.matches]; } } // Only check if we're overridden as we assume the default value is fine, // or won't be changeable (e.g. enterprise policy). if (this.overridden) { let homePages = getHomepagePref().toLowerCase(); if ( this._ignoreList.some(code => homePages.includes(code.toLowerCase())) ) { if (Services.prefs.getBoolPref(kExtensionControllerPref, false)) { if (Services.appinfo.inSafeMode) { // Add-ons don't get started in safe mode, so just abort this. // We'll get to remove them when we next start in normal mode. return; } // getSetting does not need the module to be loaded. const item = await lazy.ExtensionPreferencesManager.getSetting( "homepage_override" ); if (item && item.id) { // During startup some modules may not be loaded yet, so we load // the setting we need prior to removal. await lazy.ExtensionParent.apiManager.asyncLoadModule( "chrome_settings_overrides" ); lazy.ExtensionPreferencesManager.removeSetting( item.id, "homepage_override" ).catch(console.error); } else { // If we don't have a setting for it, we assume the pref has // been incorrectly set somehow. Services.prefs.clearUserPref(kExtensionControllerPref); Services.prefs.clearUserPref( "browser.startup.homepage_override.privateAllowed" ); } } else { this.clear(); } Services.telemetry.recordEvent( "homepage", "preference", "ignore", "saved_reset" ); } } }, onWidgetRemoved(widgetId, area) { if (widgetId == kWidgetId) { Services.prefs.setBoolPref(kWidgetRemovedPref, true); lazy.CustomizableUI.removeListener(this); } }, /** * Add the home button to the toolbar if the user just set a custom homepage. * * This should only be done once, so we check HOME_BUTTON_REMOVED_PREF which * gets set to true when the home button is removed from the toolbar. * * If the home button is already on the toolbar it won't be moved. */ _maybeAddHomeButtonToToolbar(homePage) { if ( homePage !== "about:home" && homePage !== "about:blank" && !Services.prefs.getBoolPref(kExtensionControllerPref, false) && !Services.prefs.getBoolPref(kWidgetRemovedPref, false) && !lazy.CustomizableUI.getWidget(kWidgetId).areaType ) { // Find a spot for the home button, ideally it will be in its default // position beside the stop/refresh button. // Work backwards from the URL bar since it can't be removed and put // the button after the first non-spring we find. let navbarPlacements = lazy.CustomizableUI.getWidgetIdsInArea("nav-bar"); let position = navbarPlacements.indexOf("urlbar-container"); for (let i = position - 1; i >= 0; i--) { if (!navbarPlacements[i].startsWith("customizableui-special-spring")) { position = i + 1; break; } } lazy.CustomizableUI.addWidgetToArea(kWidgetId, "nav-bar", position); } }, _addCustomizableUiListener() { if (!Services.prefs.getBoolPref(kWidgetRemovedPref, false)) { lazy.CustomizableUI.addListener(this); } }, };