diff options
Diffstat (limited to '')
-rw-r--r-- | browser/modules/HomePage.jsm | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/browser/modules/HomePage.jsm b/browser/modules/HomePage.jsm new file mode 100644 index 0000000000..3c4103f14f --- /dev/null +++ b/browser/modules/HomePage.jsm @@ -0,0 +1,359 @@ +/* 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 lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", + ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs", + ExtensionPreferencesManager: + "resource://gre/modules/ExtensionPreferencesManager.sys.mjs", + IgnoreLists: "resource://gre/modules/IgnoreLists.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +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); + } + }, +}; |