summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/preferences.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/preferences/preferences.js')
-rw-r--r--browser/components/preferences/preferences.js661
1 files changed, 661 insertions, 0 deletions
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
new file mode 100644
index 0000000000..8498a5cfb7
--- /dev/null
+++ b/browser/components/preferences/preferences.js
@@ -0,0 +1,661 @@
+/* - 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 the files imported by the .xul files.
+/* import-globals-from main.js */
+/* import-globals-from home.js */
+/* import-globals-from search.js */
+/* import-globals-from containers.js */
+/* import-globals-from privacy.js */
+/* import-globals-from sync.js */
+/* import-globals-from experimental.js */
+/* import-globals-from moreFromMozilla.js */
+/* import-globals-from findInPage.js */
+/* import-globals-from /browser/base/content/utilityOverlay.js */
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+var { Integration } = ChromeUtils.importESModule(
+ "resource://gre/modules/Integration.sys.mjs"
+);
+/* global DownloadIntegration */
+Integration.downloads.defineESModuleGetter(
+ this,
+ "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.sys.mjs"
+);
+
+var { PrivateBrowsingUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
+);
+
+var { Weave } = ChromeUtils.importESModule(
+ "resource://services-sync/main.sys.mjs"
+);
+
+var { FirefoxRelayTelemetry } = ChromeUtils.importESModule(
+ "resource://gre/modules/FirefoxRelayTelemetry.mjs"
+);
+
+var { FxAccounts, getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+var fxAccounts = getFxAccountsSingleton();
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ gApplicationUpdateService: [
+ "@mozilla.org/updates/update-service;1",
+ "nsIApplicationUpdateService",
+ ],
+
+ listManager: [
+ "@mozilla.org/url-classifier/listmanager;1",
+ "nsIUrlListManager",
+ ],
+ gHandlerService: [
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService",
+ ],
+ gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
+});
+
+if (Cc["@mozilla.org/gio-service;1"]) {
+ XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gGIOService",
+ "@mozilla.org/gio-service;1",
+ "nsIGIOService"
+ );
+} else {
+ this.gGIOService = null;
+}
+
+ChromeUtils.defineESModuleGetters(this, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
+ ContextualIdentityService:
+ "resource://gre/modules/ContextualIdentityService.sys.mjs",
+ DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+ ExtensionPreferencesManager:
+ "resource://gre/modules/ExtensionPreferencesManager.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+ FeatureGate: "resource://featuregates/FeatureGate.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ FirefoxRelay: "resource://gre/modules/FirefoxRelay.sys.mjs",
+ LangPackMatcher: "resource://gre/modules/LangPackMatcher.sys.mjs",
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
+ UIState: "resource://services-sync/UIState.sys.mjs",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
+ UrlbarProviderQuickActions:
+ "resource:///modules/UrlbarProviderQuickActions.sys.mjs",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HomePage: "resource:///modules/HomePage.jsm",
+ SelectionChangedMenulist: "resource:///modules/SelectionChangedMenulist.jsm",
+ SiteDataManager: "resource:///modules/SiteDataManager.jsm",
+ TransientPrefs: "resource:///modules/TransientPrefs.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "gSubDialog", function () {
+ const { SubDialogManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/SubDialog.sys.mjs"
+ );
+ return new SubDialogManager({
+ dialogStack: document.getElementById("dialogStack"),
+ dialogTemplate: document.getElementById("dialogTemplate"),
+ dialogOptions: {
+ styleSheets: [
+ "chrome://browser/skin/preferences/dialog.css",
+ "chrome://browser/skin/preferences/preferences.css",
+ ],
+ resizeCallback: async ({ title, frame }) => {
+ // Search within main document and highlight matched keyword.
+ await gSearchResultsPane.searchWithinNode(
+ title,
+ gSearchResultsPane.query
+ );
+
+ // Search within sub-dialog document and highlight matched keyword.
+ await gSearchResultsPane.searchWithinNode(
+ frame.contentDocument.firstElementChild,
+ gSearchResultsPane.query
+ );
+
+ // Creating tooltips for all the instances found
+ for (let node of gSearchResultsPane.listSearchTooltips) {
+ if (!node.tooltipNode) {
+ gSearchResultsPane.createSearchTooltip(
+ node,
+ gSearchResultsPane.query
+ );
+ }
+ }
+ },
+ },
+ });
+});
+
+var gLastCategory = { category: undefined, subcategory: undefined };
+const gXULDOMParser = new DOMParser();
+var gCategoryModules = new Map();
+var gCategoryInits = new Map();
+function init_category_if_required(category) {
+ let categoryInfo = gCategoryInits.get(category);
+ if (!categoryInfo) {
+ throw new Error(
+ "Unknown in-content prefs category! Can't init " + category
+ );
+ }
+ if (categoryInfo.inited) {
+ return null;
+ }
+ return categoryInfo.init();
+}
+
+function register_module(categoryName, categoryObject) {
+ gCategoryModules.set(categoryName, categoryObject);
+ gCategoryInits.set(categoryName, {
+ inited: false,
+ async init() {
+ let startTime = performance.now();
+ let template = document.getElementById("template-" + categoryName);
+ if (template) {
+ // Replace the template element with the nodes inside of it.
+ let frag = template.content;
+ await document.l10n.translateFragment(frag);
+
+ // Actually insert them into the DOM.
+ document.l10n.pauseObserving();
+ template.replaceWith(frag);
+ document.l10n.resumeObserving();
+
+ // We need to queue an update again because the previous update might
+ // have happened while we awaited on translateFragment.
+ Preferences.queueUpdateOfAllElements();
+ }
+
+ categoryObject.init();
+ this.inited = true;
+ ChromeUtils.addProfilerMarker(
+ "Preferences",
+ { startTime },
+ categoryName + " init"
+ );
+ },
+ });
+}
+
+document.addEventListener("DOMContentLoaded", init_all, { once: true });
+
+function init_all() {
+ Preferences.forceEnableInstantApply();
+
+ // Asks Preferences to queue an update of the attribute values of
+ // the entire document.
+ Preferences.queueUpdateOfAllElements();
+ Services.telemetry.setEventRecordingEnabled("aboutpreferences", true);
+
+ register_module("paneGeneral", gMainPane);
+ register_module("paneHome", gHomePane);
+ register_module("paneSearch", gSearchPane);
+ register_module("panePrivacy", gPrivacyPane);
+ register_module("paneContainers", gContainersPane);
+ if (Services.prefs.getBoolPref("browser.preferences.experimental")) {
+ // Set hidden based on previous load's hidden value.
+ document.getElementById("category-experimental").hidden =
+ Services.prefs.getBoolPref(
+ "browser.preferences.experimental.hidden",
+ false
+ );
+ register_module("paneExperimental", gExperimentalPane);
+ }
+
+ NimbusFeatures.moreFromMozilla.recordExposureEvent({ once: true });
+ if (NimbusFeatures.moreFromMozilla.getVariable("enabled")) {
+ document.getElementById("category-more-from-mozilla").hidden = false;
+ gMoreFromMozillaPane.option =
+ NimbusFeatures.moreFromMozilla.getVariable("template");
+ register_module("paneMoreFromMozilla", gMoreFromMozillaPane);
+ }
+ // The Sync category needs to be the last of the "real" categories
+ // registered and inititalized since many tests wait for the
+ // "sync-pane-loaded" observer notification before starting the test.
+ if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+ document.getElementById("category-sync").hidden = false;
+ register_module("paneSync", gSyncPane);
+ }
+ register_module("paneSearchResults", gSearchResultsPane);
+ gSearchResultsPane.init();
+ gMainPane.preInit();
+
+ let categories = document.getElementById("categories");
+ categories.addEventListener("select", event => gotoPref(event.target.value));
+
+ document.documentElement.addEventListener("keydown", function (event) {
+ if (event.keyCode == KeyEvent.DOM_VK_TAB) {
+ categories.setAttribute("keyboard-navigation", "true");
+ }
+ });
+ categories.addEventListener("mousedown", function () {
+ this.removeAttribute("keyboard-navigation");
+ });
+
+ maybeDisplayPoliciesNotice();
+
+ window.addEventListener("hashchange", onHashChange);
+
+ document.getElementById("focusSearch1").addEventListener("command", () => {
+ gSearchResultsPane.searchInput.focus();
+ });
+
+ gotoPref().then(() => {
+ document.getElementById("addonsButton").addEventListener("click", e => {
+ e.preventDefault();
+ if (e.button >= 2) {
+ // Ignore right clicks.
+ return;
+ }
+ let mainWindow = window.browsingContext.topChromeWindow;
+ mainWindow.BrowserOpenAddonsMgr();
+ });
+
+ document.dispatchEvent(
+ new CustomEvent("Initialized", {
+ bubbles: true,
+ cancelable: true,
+ })
+ );
+ });
+}
+
+function telemetryBucketForCategory(category) {
+ category = category.toLowerCase();
+ switch (category) {
+ case "containers":
+ case "general":
+ case "home":
+ case "privacy":
+ case "search":
+ case "sync":
+ case "searchresults":
+ return category;
+ default:
+ return "unknown";
+ }
+}
+
+function onHashChange() {
+ gotoPref(null, "hash");
+}
+
+async function gotoPref(
+ aCategory,
+ aShowReason = aCategory ? "click" : "initial"
+) {
+ let categories = document.getElementById("categories");
+ const kDefaultCategoryInternalName = "paneGeneral";
+ const kDefaultCategory = "general";
+ let hash = document.location.hash;
+ let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
+
+ let breakIndex = category.indexOf("-");
+ // Subcategories allow for selecting smaller sections of the preferences
+ // until proper search support is enabled (bug 1353954).
+ let subcategory = breakIndex != -1 && category.substring(breakIndex + 1);
+ if (subcategory) {
+ category = category.substring(0, breakIndex);
+ }
+ category = friendlyPrefCategoryNameToInternalName(category);
+ if (category != "paneSearchResults") {
+ gSearchResultsPane.query = null;
+ gSearchResultsPane.searchInput.value = "";
+ gSearchResultsPane.removeAllSearchIndicators(window, true);
+ } else if (!gSearchResultsPane.searchInput.value) {
+ // Something tried to send us to the search results pane without
+ // a query string. Default to the General pane instead.
+ category = kDefaultCategoryInternalName;
+ document.location.hash = kDefaultCategory;
+ gSearchResultsPane.query = null;
+ }
+
+ // Updating the hash (below) or changing the selected category
+ // will re-enter gotoPref.
+ if (gLastCategory.category == category && !subcategory) {
+ return;
+ }
+
+ let item;
+ if (category != "paneSearchResults") {
+ // Hide second level headers in normal view
+ for (let element of document.querySelectorAll(".search-header")) {
+ element.hidden = true;
+ }
+
+ item = categories.querySelector(".category[value=" + category + "]");
+ if (!item || item.hidden) {
+ category = kDefaultCategoryInternalName;
+ item = categories.querySelector(".category[value=" + category + "]");
+ }
+ }
+
+ if (
+ gLastCategory.category ||
+ category != kDefaultCategoryInternalName ||
+ subcategory
+ ) {
+ let friendlyName = internalPrefCategoryNameToFriendlyName(category);
+ // Overwrite the hash, unless there is no hash and we're switching to the
+ // default category, e.g. by using the 'back' button after navigating to
+ // a different category.
+ if (
+ !(!document.location.hash && category == kDefaultCategoryInternalName)
+ ) {
+ document.location.hash = friendlyName;
+ }
+ }
+ // Need to set the gLastCategory before setting categories.selectedItem since
+ // the categories 'select' event will re-enter the gotoPref codepath.
+ gLastCategory.category = category;
+ gLastCategory.subcategory = subcategory;
+ if (item) {
+ categories.selectedItem = item;
+ } else {
+ categories.clearSelection();
+ }
+ window.history.replaceState(category, document.title);
+
+ try {
+ await init_category_if_required(category);
+ } catch (ex) {
+ console.error(
+ new Error(
+ "Error initializing preference category " + category + ": " + ex
+ )
+ );
+ throw ex;
+ }
+
+ // Bail out of this goToPref if the category
+ // or subcategory changed during async operation.
+ if (
+ gLastCategory.category !== category ||
+ gLastCategory.subcategory !== subcategory
+ ) {
+ return;
+ }
+
+ search(category, "data-category");
+
+ if (aShowReason != "initial") {
+ document.querySelector(".main-content").scrollTop = 0;
+ }
+
+ // Check to see if the category module wants to do any special
+ // handling of the subcategory - for example, opening a SubDialog.
+ //
+ // If not, just do a normal spotlight on the subcategory.
+ let categoryModule = gCategoryModules.get(category);
+ if (!categoryModule.handleSubcategory?.(subcategory)) {
+ spotlight(subcategory, category);
+ }
+
+ // Record which category is shown
+ Services.telemetry.recordEvent(
+ "aboutpreferences",
+ "show",
+ aShowReason,
+ category
+ );
+}
+
+function search(aQuery, aAttribute) {
+ let mainPrefPane = document.getElementById("mainPrefPane");
+ let elements = mainPrefPane.children;
+ for (let element of elements) {
+ // If the "data-hidden-from-search" is "true", the
+ // element will not get considered during search.
+ if (
+ element.getAttribute("data-hidden-from-search") != "true" ||
+ element.getAttribute("data-subpanel") == "true"
+ ) {
+ let attributeValue = element.getAttribute(aAttribute);
+ if (attributeValue == aQuery) {
+ element.hidden = false;
+ } else {
+ element.hidden = true;
+ }
+ } else if (
+ element.getAttribute("data-hidden-from-search") == "true" &&
+ !element.hidden
+ ) {
+ element.hidden = true;
+ }
+ element.classList.remove("visually-hidden");
+ }
+
+ let keysets = mainPrefPane.getElementsByTagName("keyset");
+ for (let element of keysets) {
+ let attributeValue = element.getAttribute(aAttribute);
+ if (attributeValue == aQuery) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", true);
+ }
+ }
+}
+
+async function spotlight(subcategory, category) {
+ let highlightedElements = document.querySelectorAll(".spotlight");
+ if (highlightedElements.length) {
+ for (let element of highlightedElements) {
+ element.classList.remove("spotlight");
+ }
+ }
+ if (subcategory) {
+ scrollAndHighlight(subcategory, category);
+ }
+}
+
+async function scrollAndHighlight(subcategory, category) {
+ let element = document.querySelector(`[data-subcategory="${subcategory}"]`);
+ if (!element) {
+ return;
+ }
+ let header = getClosestDisplayedHeader(element);
+
+ scrollContentTo(header);
+ element.classList.add("spotlight");
+}
+
+/**
+ * If there is no visible second level header it will return first level header,
+ * otherwise return second level header.
+ * @returns {Element} - The closest displayed header.
+ */
+function getClosestDisplayedHeader(element) {
+ let header = element.closest("groupbox");
+ let searchHeader = header.querySelector(".search-header");
+ if (
+ searchHeader &&
+ searchHeader.hidden &&
+ header.previousElementSibling.classList.contains("subcategory")
+ ) {
+ header = header.previousElementSibling;
+ }
+ return header;
+}
+
+function scrollContentTo(element) {
+ const STICKY_CONTAINER_HEIGHT =
+ document.querySelector(".sticky-container").clientHeight;
+ let mainContent = document.querySelector(".main-content");
+ let top = element.getBoundingClientRect().top - STICKY_CONTAINER_HEIGHT;
+ mainContent.scroll({
+ top,
+ behavior: "smooth",
+ });
+}
+
+function friendlyPrefCategoryNameToInternalName(aName) {
+ if (aName.startsWith("pane")) {
+ return aName;
+ }
+ return "pane" + aName.substring(0, 1).toUpperCase() + aName.substr(1);
+}
+
+// This function is duplicated inside of utilityOverlay.js's openPreferences.
+function internalPrefCategoryNameToFriendlyName(aName) {
+ return (aName || "").replace(/^pane./, function (toReplace) {
+ return toReplace[4].toLowerCase();
+ });
+}
+
+// Put up a confirm dialog with "ok to restart", "revert without restarting"
+// and "restart later" buttons and returns the index of the button chosen.
+// We can choose not to display the "restart later", or "revert" buttons,
+// altough the later still lets us revert by using the escape key.
+//
+// The constants are useful to interpret the return value of the function.
+const CONFIRM_RESTART_PROMPT_RESTART_NOW = 0;
+const CONFIRM_RESTART_PROMPT_CANCEL = 1;
+const CONFIRM_RESTART_PROMPT_RESTART_LATER = 2;
+async function confirmRestartPrompt(
+ aRestartToEnable,
+ aDefaultButtonIndex,
+ aWantRevertAsCancelButton,
+ aWantRestartLaterButton
+) {
+ let [
+ msg,
+ title,
+ restartButtonText,
+ noRestartButtonText,
+ restartLaterButtonText,
+ ] = await document.l10n.formatValues([
+ {
+ id: aRestartToEnable
+ ? "feature-enable-requires-restart"
+ : "feature-disable-requires-restart",
+ },
+ { id: "should-restart-title" },
+ { id: "should-restart-ok" },
+ { id: "cancel-no-restart-button" },
+ { id: "restart-later" },
+ ]);
+
+ // Set up the first (index 0) button:
+ let buttonFlags =
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING;
+
+ // Set up the second (index 1) button:
+ if (aWantRevertAsCancelButton) {
+ buttonFlags +=
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING;
+ } else {
+ noRestartButtonText = null;
+ buttonFlags +=
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
+ }
+
+ // Set up the third (index 2) button:
+ if (aWantRestartLaterButton) {
+ buttonFlags +=
+ Services.prompt.BUTTON_POS_2 * Services.prompt.BUTTON_TITLE_IS_STRING;
+ } else {
+ restartLaterButtonText = null;
+ }
+
+ switch (aDefaultButtonIndex) {
+ case 0:
+ buttonFlags += Services.prompt.BUTTON_POS_0_DEFAULT;
+ break;
+ case 1:
+ buttonFlags += Services.prompt.BUTTON_POS_1_DEFAULT;
+ break;
+ case 2:
+ buttonFlags += Services.prompt.BUTTON_POS_2_DEFAULT;
+ break;
+ default:
+ break;
+ }
+
+ let buttonIndex = Services.prompt.confirmEx(
+ window,
+ title,
+ msg,
+ buttonFlags,
+ restartButtonText,
+ noRestartButtonText,
+ restartLaterButtonText,
+ null,
+ {}
+ );
+
+ // If we have the second confirmation dialog for restart, see if the user
+ // cancels out at that point.
+ if (buttonIndex == CONFIRM_RESTART_PROMPT_RESTART_NOW) {
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+ if (cancelQuit.data) {
+ buttonIndex = CONFIRM_RESTART_PROMPT_CANCEL;
+ }
+ }
+ return buttonIndex;
+}
+
+// This function is used to append search keywords found
+// in the related subdialog to the button that will activate the subdialog.
+function appendSearchKeywords(aId, keywords) {
+ let element = document.getElementById(aId);
+ let searchKeywords = element.getAttribute("searchkeywords");
+ if (searchKeywords) {
+ keywords.push(searchKeywords);
+ }
+ element.setAttribute("searchkeywords", keywords.join(" "));
+}
+
+async function ensureScrollPadding() {
+ let stickyContainer = document.querySelector(".sticky-container");
+ let height = await window.browsingContext.topChromeWindow
+ .promiseDocumentFlushed(() => stickyContainer.clientHeight)
+ .catch(err => Cu.reportError); // Can reject if the window goes away.
+
+ // Make it a bit more, to ensure focus rectangles etc. don't get cut off.
+ // This being 8px causes us to end up with 90px if the policies container
+ // is not visible (the common case), which matches the CSS and thus won't
+ // cause a style change, repaint, or other changes.
+ height += 8;
+ stickyContainer
+ .closest(".main-content")
+ .style.setProperty("scroll-padding-top", height + "px");
+}
+
+function maybeDisplayPoliciesNotice() {
+ if (Services.policies.status == Services.policies.ACTIVE) {
+ document.getElementById("policies-container").removeAttribute("hidden");
+ ensureScrollPadding();
+ }
+}