gecko-dev/browser/modules/HomePage.sys.mjs

344 lines
11 KiB
JavaScript

/* 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/. */
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.
*/
export 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;
}
// 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.`
);
Glean.homepage.preferenceIgnore.record({ value: "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();
}
Glean.homepage.preferenceIgnore.record({ value: "saved_reset" });
}
}
},
onWidgetRemoved(widgetId) {
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);
}
},
};