Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
/* import-globals-from extensionControlled.js */
/* import-globals-from preferences.js */
/* import-globals-from main.js */
// HOME PAGE
/*
* Preferences:
*
* browser.startup.homepage
* - the user's home page, as a string; if the home page is a set of tabs,
* this will be those URLs separated by the pipe character "|"
* browser.newtabpage.enabled
* - determines that is shown on the user's new tab page.
* true = Activity Stream is shown,
* false = about:blank is shown
*/
Preferences.addAll([
{ id: "browser.startup.homepage", type: "wstring" },
{ id: "pref.browser.homepage.disable_button.current_page", type: "bool" },
{ id: "pref.browser.homepage.disable_button.bookmark_page", type: "bool" },
{ id: "pref.browser.homepage.disable_button.restore_default", type: "bool" },
{ id: "browser.newtabpage.enabled", type: "bool" },
]);
const HOMEPAGE_OVERRIDE_KEY = "homepage_override";
const URL_OVERRIDES_TYPE = "url_overrides";
const NEW_TAB_KEY = "newTabURL";
const BLANK_HOMEPAGE_URL = "chrome://browser/content/blanktab.html";
var gHomePane = {
HOME_MODE_FIREFOX_HOME: "0",
HOME_MODE_BLANK: "1",
HOME_MODE_CUSTOM: "2",
HOMEPAGE_PREF: "browser.startup.homepage",
NEWTAB_ENABLED_PREF: "browser.newtabpage.enabled",
ACTIVITY_STREAM_PREF_BRANCH: "browser.newtabpage.activity-stream.",
get homePanePrefs() {
return Preferences.getAll().filter(pref =>
pref.id.includes(this.ACTIVITY_STREAM_PREF_BRANCH)
);
},
get isPocketNewtabEnabled() {
const value = Services.prefs.getStringPref(
"browser.newtabpage.activity-stream.discoverystream.config",
""
);
if (value) {
try {
return JSON.parse(value).enabled;
} catch (e) {
console.error("Failed to parse Discovery Stream pref.");
}
}
return false;
},
async syncToNewTabPref() {
let menulist = document.getElementById("newTabMode");
if (["0", "1"].includes(menulist.value)) {
let newtabEnabledPref = Services.prefs.getBoolPref(
this.NEWTAB_ENABLED_PREF,
true
);
let newValue = menulist.value !== this.HOME_MODE_BLANK;
// Only set this if the pref has changed, otherwise the pref change will trigger other listeners to repeat.
if (newtabEnabledPref !== newValue) {
Services.prefs.setBoolPref(this.NEWTAB_ENABLED_PREF, newValue);
}
let selectedAddon = ExtensionSettingsStore.getSetting(
URL_OVERRIDES_TYPE,
NEW_TAB_KEY
);
if (selectedAddon) {
ExtensionSettingsStore.select(null, URL_OVERRIDES_TYPE, NEW_TAB_KEY);
}
} else {
let addon = await AddonManager.getAddonByID(menulist.value);
if (addon && addon.isActive) {
ExtensionSettingsStore.select(
addon.id,
URL_OVERRIDES_TYPE,
NEW_TAB_KEY
);
}
}
},
async syncFromNewTabPref() {
let menulist = document.getElementById("newTabMode");
// If the new tab url was changed to about:blank or about:newtab
if (
AboutNewTab.newTabURL === "about:newtab" ||
AboutNewTab.newTabURL === "about:blank" ||
AboutNewTab.newTabURL === BLANK_HOMEPAGE_URL
) {
let newtabEnabledPref = Services.prefs.getBoolPref(
this.NEWTAB_ENABLED_PREF,
true
);
let newValue = newtabEnabledPref
? this.HOME_MODE_FIREFOX_HOME
: this.HOME_MODE_BLANK;
if (newValue !== menulist.value) {
menulist.value = newValue;
}
menulist.disabled = Preferences.get(this.NEWTAB_ENABLED_PREF).locked;
// If change was triggered by installing an addon we need to update
// the value of the menulist to be that addon.
} else {
let selectedAddon = ExtensionSettingsStore.getSetting(
URL_OVERRIDES_TYPE,
NEW_TAB_KEY
);
if (selectedAddon && menulist.value !== selectedAddon.id) {
menulist.value = selectedAddon.id;
}
}
},
/**
* _updateMenuInterface: adds items to or removes them from the menulists
* @param {string} selectId Optional Id of the menulist to add or remove items from.
* If not included this will update both home and newtab menus.
*/
async _updateMenuInterface(selectId) {
let selects;
if (selectId) {
selects = [document.getElementById(selectId)];
} else {
let newTabSelect = document.getElementById("newTabMode");
let homeSelect = document.getElementById("homeMode");
selects = [homeSelect, newTabSelect];
}
for (let select of selects) {
// Remove addons from the menu popup which are no longer installed, or disabled.
// let menuOptions = select.menupopup.childNodes;
let menuOptions = Array.from(select.menupopup.childNodes);
for (let option of menuOptions) {
// If the value is not a number, assume it is an addon ID
if (!/^\d+$/.test(option.value)) {
let addon = await AddonManager.getAddonByID(option.value);
if (option && (!addon || !addon.isActive)) {
option.remove();
}
}
}
let extensionOptions;
await ExtensionSettingsStore.initialize();
if (select.id === "homeMode") {
extensionOptions = ExtensionSettingsStore.getAllSettings(
PREF_SETTING_TYPE,
HOMEPAGE_OVERRIDE_KEY
);
} else {
extensionOptions = ExtensionSettingsStore.getAllSettings(
URL_OVERRIDES_TYPE,
NEW_TAB_KEY
);
}
let addons = await AddonManager.getAddonsByIDs(
extensionOptions.map(a => a.id)
);
// Add addon options to the menu popups
let menupopup = select.querySelector("menupopup");
for (let addon of addons) {
if (!addon || !addon.id || !addon.isActive) {
continue;
}
let currentOption = select.querySelector(
`[value="${CSS.escape(addon.id)}"]`
);
if (!currentOption) {
let option = document.createXULElement("menuitem");
option.classList.add("addon-with-favicon");
option.value = addon.id;
option.label = addon.name;
menupopup.append(option);
option.querySelector("image").src = addon.iconURL;
}
let setting = extensionOptions.find(o => o.id == addon.id);
if (
(select.id === "homeMode" && setting.value == HomePage.get()) ||
(select.id === "newTabMode" && setting.value == AboutNewTab.newTabURL)
) {
select.value = addon.id;
}
}
}
},
/**
* watchNewTab: Listen for changes to the new tab url and enable/disable appropriate
* areas of the UI.
*/
watchNewTab() {
let newTabObserver = () => {
this.syncFromNewTabPref();
this._updateMenuInterface("newTabMode");
};
Services.obs.addObserver(newTabObserver, "newtab-url-changed");
window.addEventListener("unload", () => {
Services.obs.removeObserver(newTabObserver, "newtab-url-changed");
});
},
/**
* watchHomePrefChange: Listen for preferences changes on the Home Tab in order to
* show the appropriate home menu selection.
*/
watchHomePrefChange() {
const homePrefObserver = (subject, topic, data) => {
// only update this UI if it is exactly the HOMEPAGE_PREF, not other prefs with the same root.
if (data && data != this.HOMEPAGE_PREF) {
return;
}
this._updateUseCurrentButton();
this._renderCustomSettings();
this._handleHomePageOverrides();
this._updateMenuInterface("homeMode");
};
Services.prefs.addObserver(this.HOMEPAGE_PREF, homePrefObserver);
window.addEventListener("unload", () => {
Services.prefs.removeObserver(this.HOMEPAGE_PREF, homePrefObserver);
});
},
/**
* Listen extension changes on the New Tab and Home Tab
* in order to update the UI and show or hide the Restore Defaults button.
*/
watchExtensionPrefChange() {
const extensionSettingChanged = (evt, setting) => {
if (setting.key == "homepage_override" && setting.type == "prefs") {
this._updateMenuInterface("homeMode");
} else if (
setting.key == "newTabURL" &&
setting.type == "url_overrides"
) {
this._updateMenuInterface("newTabMode");
}
};
Management.on("extension-setting-changed", extensionSettingChanged);
window.addEventListener("unload", () => {
Management.off("extension-setting-changed", extensionSettingChanged);
});
},
/**
* Listen for all preferences changes on the Home Tab in order to show or
* hide the Restore Defaults button.
*/
watchHomeTabPrefChange() {
const observer = () => this.toggleRestoreDefaultsBtn();
Services.prefs.addObserver(this.ACTIVITY_STREAM_PREF_BRANCH, observer);
Services.prefs.addObserver(this.HOMEPAGE_PREF, observer);
Services.prefs.addObserver(this.NEWTAB_ENABLED_PREF, observer);
window.addEventListener("unload", () => {
Services.prefs.removeObserver(this.ACTIVITY_STREAM_PREF_BRANCH, observer);
Services.prefs.removeObserver(this.HOMEPAGE_PREF, observer);
Services.prefs.removeObserver(this.NEWTAB_ENABLED_PREF, observer);
});
},
/**
* _renderCustomSettings: Hides or shows the UI for setting a custom
* homepage URL
* @param {obj} options
* @param {bool} options.shouldShow Should the custom UI be shown?
* @param {bool} options.isControlled Is an extension controlling the home page?
*/
_renderCustomSettings(options = {}) {
let { shouldShow, isControlled } = options;
const customSettingsContainerEl = document.getElementById("customSettings");
const customUrlEl = document.getElementById("homePageUrl");
const homePage = HomePage.get();
const isHomePageCustom =
(!this._isHomePageDefaultValue() &&
!this.isHomePageBlank() &&
!isControlled) ||
homePage.locked;
if (typeof shouldShow === "undefined") {
shouldShow = isHomePageCustom;
}
customSettingsContainerEl.hidden = !shouldShow;
// We can't use isHomePageDefaultValue and isHomePageBlank here because we want to disregard the blank
// possibility triggered by the browser.startup.page being 0.
// We also skip when HomePage is locked because it might be locked to a default that isn't "about:home"
// (and it makes existing tests happy).
let newValue;
if (
this._isBlankPage(homePage) ||
(HomePage.isDefault && !HomePage.locked)
) {
newValue = "";
} else {
newValue = homePage;
}
if (customUrlEl.value !== newValue) {
customUrlEl.value = newValue;
}
},
/**
* _isHomePageDefaultValue
* @returns {bool} Is the homepage set to the default pref value?
*/
_isHomePageDefaultValue() {
const startupPref = Preferences.get("browser.startup.page");
return (
startupPref.value !== gMainPane.STARTUP_PREF_BLANK && HomePage.isDefault
);
},
/**
* isHomePageBlank
* @returns {bool} Is the homepage set to about:blank?
*/
isHomePageBlank() {
const startupPref = Preferences.get("browser.startup.page");
return (
["about:blank", BLANK_HOMEPAGE_URL, ""].includes(HomePage.get()) ||
startupPref.value === gMainPane.STARTUP_PREF_BLANK
);
},
/**
* _isTabAboutPreferencesOrSettings: Is a given tab set to about:preferences or about:settings?
* @param {Element} aTab A tab element
* @returns {bool} Is the linkedBrowser of aElement set to about:preferences or about:settings?
*/
_isTabAboutPreferencesOrSettings(aTab) {
return (
aTab.linkedBrowser.currentURI.spec.startsWith("about:preferences") ||
aTab.linkedBrowser.currentURI.spec.startsWith("about:settings")
);
},
/**
* _getTabsForHomePage
* @returns {Array} An array of current tabs
*/
_getTabsForHomePage() {
let tabs = [];
let win = Services.wm.getMostRecentWindow("navigator:browser");
// We should only include visible & non-pinned tabs
if (
win &&
win.document.documentElement.getAttribute("windowtype") ===
"navigator:browser"
) {
tabs = win.gBrowser.visibleTabs.slice(win.gBrowser._numPinnedTabs);
tabs = tabs.filter(tab => !this._isTabAboutPreferencesOrSettings(tab));
// XXX: Bug 1441637 - Fix tabbrowser to report tab.closing before it blurs it
tabs = tabs.filter(tab => !tab.closing);
}
return tabs;
},
_renderHomepageMode(controllingExtension) {
const isDefault = this._isHomePageDefaultValue();
const isBlank = this.isHomePageBlank();
const el = document.getElementById("homeMode");
let newValue;
if (controllingExtension && controllingExtension.id) {
newValue = controllingExtension.id;
} else if (isDefault) {
newValue = this.HOME_MODE_FIREFOX_HOME;
} else if (isBlank) {
newValue = this.HOME_MODE_BLANK;
} else {
newValue = this.HOME_MODE_CUSTOM;
}
if (el.value !== newValue) {
el.value = newValue;
}
},
_setInputDisabledStates(isControlled) {
let tabCount = this._getTabsForHomePage().length;
// Disable or enable the inputs based on if this is controlled by an extension.
document
.querySelectorAll(".check-home-page-controlled")
.forEach(element => {
let isDisabled;
let pref =
element.getAttribute("preference") ||
element.getAttribute("data-preference-related");
if (!pref) {
throw new Error(
`Element with id ${element.id} did not have preference or data-preference-related attribute defined.`
);
}
if (pref === this.HOMEPAGE_PREF) {
isDisabled = HomePage.locked;
} else {
isDisabled = Preferences.get(pref).locked || isControlled;
}
if (pref === "pref.browser.disable_button.current_page") {
// Special case for current_page to disable it if tabCount is 0
isDisabled = isDisabled || tabCount < 1;
}
element.disabled = isDisabled;
});
},
async _handleHomePageOverrides() {
let controllingExtension;
if (HomePage.locked) {
// Disable inputs if they are locked.
this._renderCustomSettings();
this._setInputDisabledStates(false);
} else {
if (HomePage.get().startsWith("moz-extension:")) {
controllingExtension = await getControllingExtension(
PREF_SETTING_TYPE,
HOMEPAGE_OVERRIDE_KEY
);
}
this._setInputDisabledStates();
this._renderCustomSettings({
isControlled: !!controllingExtension,
});
}
this._renderHomepageMode(controllingExtension);
},
onMenuChange(event) {
const { value } = event.target;
const startupPref = Preferences.get("browser.startup.page");
let selectedAddon = ExtensionSettingsStore.getSetting(
PREF_SETTING_TYPE,
HOMEPAGE_OVERRIDE_KEY
);
switch (value) {
case this.HOME_MODE_FIREFOX_HOME:
if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) {
startupPref.value = gMainPane.STARTUP_PREF_HOMEPAGE;
}
if (!HomePage.isDefault) {
HomePage.reset();
} else {
this._renderCustomSettings({ shouldShow: false });
}
if (selectedAddon) {
ExtensionSettingsStore.select(
null,
PREF_SETTING_TYPE,
HOMEPAGE_OVERRIDE_KEY
);
}
break;
case this.HOME_MODE_BLANK:
if (!this._isBlankPage(HomePage.get())) {
HomePage.safeSet(BLANK_HOMEPAGE_URL);
} else {
this._renderCustomSettings({ shouldShow: false });
}
if (selectedAddon) {
ExtensionSettingsStore.select(
null,
PREF_SETTING_TYPE,
HOMEPAGE_OVERRIDE_KEY
);
}
break;
case this.HOME_MODE_CUSTOM:
if (startupPref.value === gMainPane.STARTUP_PREF_BLANK) {
Services.prefs.clearUserPref(startupPref.id);
}
if (HomePage.getDefault() != HomePage.getOriginalDefault()) {
HomePage.clear();
}
this._renderCustomSettings({ shouldShow: true });
if (selectedAddon) {
ExtensionSettingsStore.select(
null,
PREF_SETTING_TYPE,
HOMEPAGE_OVERRIDE_KEY
);
}
break;
// extensions will have a variety of values as their ID, so treat it as default
default:
AddonManager.getAddonByID(value).then(addon => {
if (addon && addon.isActive) {
ExtensionPreferencesManager.selectSetting(
addon.id,
HOMEPAGE_OVERRIDE_KEY
);
}
this._renderCustomSettings({ shouldShow: false });
});
}
},
/**
* Switches the "Use Current Page" button between its singular and plural
* forms.
*/
async _updateUseCurrentButton() {
let useCurrent = document.getElementById("useCurrentBtn");
let tabs = this._getTabsForHomePage();
const tabCount = tabs.length;
document.l10n.setAttributes(useCurrent, "use-current-pages", { tabCount });
// If the homepage is controlled by an extension then you can't use this.
if (
await getControllingExtensionInfo(
PREF_SETTING_TYPE,
HOMEPAGE_OVERRIDE_KEY
)
) {
return;
}
// In this case, the button's disabled state is set by preferences.xml.
let prefName = "pref.browser.homepage.disable_button.current_page";
if (Preferences.get(prefName).locked) {
return;
}
useCurrent.disabled = tabCount < 1;
},
/**
* Sets the home page to the URL(s) of any currently opened tab(s),
* updating about:preferences#home UI to reflect this.
*/
setHomePageToCurrent() {
let tabs = this._getTabsForHomePage();
function getTabURI(t) {
return t.linkedBrowser.currentURI.spec;
}
// FIXME Bug 244192: using dangerous "|" joiner!
if (tabs.length) {
HomePage.set(tabs.map(getTabURI).join("|")).catch(console.error);
}
},
_setHomePageToBookmarkClosed(rv, aEvent) {
if (aEvent.detail.button != "accept") {
return;
}
if (rv.urls && rv.names) {
// XXX still using dangerous "|" joiner!
HomePage.set(rv.urls.join("|")).catch(console.error);
}
},
/**
* Displays a dialog in which the user can select a bookmark to use as home
* page. If the user selects a bookmark, that bookmark's name is displayed in
* UI and the bookmark's address is stored to the home page preference.
*/
setHomePageToBookmark() {
const rv = { urls: null, names: null };
gSubDialog.open(
{
features: "resizable=yes, modal=yes",
closingCallback: this._setHomePageToBookmarkClosed.bind(this, rv),
},
rv
);
},
restoreDefaultHomePage() {
HomePage.reset();
this._handleHomePageOverrides();
Services.prefs.clearUserPref(this.NEWTAB_ENABLED_PREF);
AboutNewTab.resetNewTabURL();
},
onCustomHomePageChange(event) {
const value = event.target.value || HomePage.getDefault();
HomePage.set(value).catch(console.error);
},
/**
* Check all Home Tab preferences for user set values.
*/
_changedHomeTabDefaultPrefs() {
// If Discovery Stream is enabled Firefox Home Content preference options are hidden
const homeContentChanged =
!this.isPocketNewtabEnabled &&
this.homePanePrefs.some(pref => pref.hasUserValue);
const newtabPref = Preferences.get(this.NEWTAB_ENABLED_PREF);
const extensionControlled = Preferences.get(
"browser.startup.homepage_override.extensionControlled"
);
return (
homeContentChanged ||
HomePage.overridden ||
newtabPref.hasUserValue ||
AboutNewTab.newTabURLOverridden ||
extensionControlled
);
},
_isBlankPage(url) {
return url == "about:blank" || url == BLANK_HOMEPAGE_URL;
},
/**
* Show the Restore Defaults button if any preference on the Home tab was
* changed, or hide it otherwise.
*/
toggleRestoreDefaultsBtn() {
const btn = document.getElementById("restoreDefaultHomePageBtn");
const prefChanged = this._changedHomeTabDefaultPrefs();
if (prefChanged) {
btn.style.removeProperty("visibility");
} else {
btn.style.visibility = "hidden";
}
},
/**
* Set all prefs on the Home tab back to their default values.
*/
restoreDefaultPrefsForHome() {
this.restoreDefaultHomePage();
// If Discovery Stream is enabled Firefox Home Content preference options are hidden
if (!this.isPocketNewtabEnabled) {
this.homePanePrefs.forEach(pref => Services.prefs.clearUserPref(pref.id));
}
},
init() {
// Event Listeners
document
.getElementById("homePageUrl")
.addEventListener("change", this.onCustomHomePageChange.bind(this));
document
.getElementById("useCurrentBtn")
.addEventListener("command", this.setHomePageToCurrent.bind(this));
document
.getElementById("useBookmarkBtn")
.addEventListener("command", this.setHomePageToBookmark.bind(this));
document
.getElementById("restoreDefaultHomePageBtn")
.addEventListener("command", this.restoreDefaultPrefsForHome.bind(this));
// Setup the add-on options for the new tab section before registering the
// listener.
this._updateMenuInterface();
document
.getElementById("newTabMode")
.addEventListener("command", this.syncToNewTabPref.bind(this));
document
.getElementById("homeMode")
.addEventListener("command", this.onMenuChange.bind(this));
this._updateUseCurrentButton();
this._handleHomePageOverrides();
this.syncFromNewTabPref();
window.addEventListener("focus", this._updateUseCurrentButton.bind(this));
// Extension/override-related events
this.watchNewTab();
this.watchHomePrefChange();
this.watchExtensionPrefChange();
this.watchHomeTabPrefChange();
// Notify observers that the UI is now ready
Services.obs.notifyObservers(window, "home-pane-loaded");
},
};