diff options
Diffstat (limited to 'browser/components/preferences/main.js')
-rw-r--r-- | browser/components/preferences/main.js | 4258 |
1 files changed, 4258 insertions, 0 deletions
diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js new file mode 100644 index 0000000000..3f5db88709 --- /dev/null +++ b/browser/components/preferences/main.js @@ -0,0 +1,4258 @@ +/* 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 /toolkit/mozapps/preferences/fontbuilder.js */ +/* import-globals-from /browser/base/content/aboutDialog-appUpdater.js */ +/* global MozXULElement */ + +ChromeUtils.defineESModuleGetters(this, { + BackgroundUpdate: "resource://gre/modules/BackgroundUpdate.sys.mjs", + MigrationUtils: "resource:///modules/MigrationUtils.sys.mjs", +}); + +// Constants & Enumeration Values +const TYPE_PDF = "application/pdf"; + +const PREF_PDFJS_DISABLED = "pdfjs.disabled"; + +// Pref for when containers is being controlled +const PREF_CONTAINERS_EXTENSION = "privacy.userContext.extension"; + +// Strings to identify ExtensionSettingsStore overrides +const CONTAINERS_KEY = "privacy.containers"; + +const PREF_USE_SYSTEM_COLORS = "browser.display.use_system_colors"; +const PREF_CONTENT_APPEARANCE = + "layout.css.prefers-color-scheme.content-override"; +const FORCED_COLORS_QUERY = matchMedia("(forced-colors)"); + +const AUTO_UPDATE_CHANGED_TOPIC = + UpdateUtils.PER_INSTALLATION_PREFS["app.update.auto"].observerTopic; +const BACKGROUND_UPDATE_CHANGED_TOPIC = + UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"] + .observerTopic; + +const ICON_URL_APP = + AppConstants.platform == "linux" + ? "moz-icon://dummy.exe?size=16" + : "chrome://browser/skin/preferences/application.png"; + +// For CSS. Can be one of "ask", "save" or "handleInternally". If absent, the icon URL +// was set by us to a custom handler icon and CSS should not try to override it. +const APP_ICON_ATTR_NAME = "appHandlerIcon"; + +Preferences.addAll([ + // Startup + { id: "browser.startup.page", type: "int" }, + { id: "browser.privatebrowsing.autostart", type: "bool" }, + + // Downloads + { id: "browser.download.useDownloadDir", type: "bool", inverted: true }, + { id: "browser.download.always_ask_before_handling_new_types", type: "bool" }, + { id: "browser.download.folderList", type: "int" }, + { id: "browser.download.dir", type: "file" }, + + /* Tab preferences + Preferences: + + browser.link.open_newwindow + 1 opens such links in the most recent window or tab, + 2 opens such links in a new window, + 3 opens such links in a new tab + browser.tabs.loadInBackground + - true if display should switch to a new tab which has been opened from a + link, false if display shouldn't switch + browser.tabs.warnOnClose + - true if when closing a window with multiple tabs the user is warned and + allowed to cancel the action, false to just close the window + browser.tabs.warnOnOpen + - true if the user should be warned if he attempts to open a lot of tabs at + once (e.g. a large folder of bookmarks), false otherwise + browser.warnOnQuitShortcut + - true if the user should be warned if they quit using the keyboard shortcut + browser.taskbar.previews.enable + - true if tabs are to be shown in the Windows 7 taskbar + */ + + { id: "browser.link.open_newwindow", type: "int" }, + { id: "browser.tabs.loadInBackground", type: "bool", inverted: true }, + { id: "browser.tabs.warnOnClose", type: "bool" }, + { id: "browser.warnOnQuitShortcut", type: "bool" }, + { id: "browser.tabs.warnOnOpen", type: "bool" }, + { id: "browser.ctrlTab.sortByRecentlyUsed", type: "bool" }, + + // CFR + { + id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", + type: "bool", + }, + { + id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + type: "bool", + }, + + // Fonts + { id: "font.language.group", type: "wstring" }, + + // Languages + { id: "browser.translation.detectLanguage", type: "bool" }, + { id: "intl.regional_prefs.use_os_locales", type: "bool" }, + + // General tab + + /* Accessibility + * accessibility.browsewithcaret + - true enables keyboard navigation and selection within web pages using a + visible caret, false uses normal keyboard navigation with no caret + * accessibility.typeaheadfind + - when set to true, typing outside text areas and input boxes will + automatically start searching for what's typed within the current + document; when set to false, no search action happens */ + { id: "accessibility.browsewithcaret", type: "bool" }, + { id: "accessibility.typeaheadfind", type: "bool" }, + { id: "accessibility.blockautorefresh", type: "bool" }, + + /* Browsing + * general.autoScroll + - when set to true, clicking the scroll wheel on the mouse activates a + mouse mode where moving the mouse down scrolls the document downward with + speed correlated with the distance of the cursor from the original + position at which the click occurred (and likewise with movement upward); + if false, this behavior is disabled + * general.smoothScroll + - set to true to enable finer page scrolling than line-by-line on page-up, + page-down, and other such page movements */ + { id: "general.autoScroll", type: "bool" }, + { id: "general.smoothScroll", type: "bool" }, + { id: "widget.gtk.overlay-scrollbars.enabled", type: "bool", inverted: true }, + { id: "layout.spellcheckDefault", type: "int" }, + + { + id: "browser.preferences.defaultPerformanceSettings.enabled", + type: "bool", + }, + { id: "dom.ipc.processCount", type: "int" }, + { id: "dom.ipc.processCount.web", type: "int" }, + { id: "layers.acceleration.disabled", type: "bool", inverted: true }, + + // Files and Applications + { id: "pref.downloads.disable_button.edit_actions", type: "bool" }, + + // DRM content + { id: "media.eme.enabled", type: "bool" }, + + // Update + { id: "browser.preferences.advanced.selectedTabIndex", type: "int" }, + { id: "browser.search.update", type: "bool" }, + + { id: "privacy.userContext.enabled", type: "bool" }, + { + id: "privacy.userContext.newTabContainerOnLeftClick.enabled", + type: "bool", + }, + + // Picture-in-Picture + { + id: "media.videocontrols.picture-in-picture.video-toggle.enabled", + type: "bool", + }, + + // Media + { id: "media.hardwaremediakeys.enabled", type: "bool" }, +]); + +if (AppConstants.HAVE_SHELL_SERVICE) { + Preferences.addAll([ + { id: "browser.shell.checkDefaultBrowser", type: "bool" }, + { id: "pref.general.disable_button.default_browser", type: "bool" }, + ]); +} + +if (AppConstants.platform === "win") { + Preferences.addAll([ + { id: "browser.taskbar.previews.enable", type: "bool" }, + { id: "ui.osk.enabled", type: "bool" }, + ]); +} + +if (AppConstants.MOZ_UPDATER) { + Preferences.addAll([ + { id: "app.update.disable_button.showUpdateHistory", type: "bool" }, + ]); + + if (AppConstants.NIGHTLY_BUILD) { + Preferences.addAll([{ id: "app.update.suppressPrompts", type: "bool" }]); + } + + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + Preferences.addAll([{ id: "app.update.service.enabled", type: "bool" }]); + } +} + +XPCOMUtils.defineLazyGetter(this, "gIsPackagedApp", () => { + return Services.sysinfo.getProperty("isPackagedApp"); +}); + +// A promise that resolves when the list of application handlers is loaded. +// We store this in a global so tests can await it. +var promiseLoadHandlersList; + +// Load the preferences string bundle for other locales with fallbacks. +function getBundleForLocales(newLocales) { + let locales = Array.from( + new Set([ + ...newLocales, + ...Services.locale.requestedLocales, + Services.locale.lastFallbackLocale, + ]) + ); + return new Localization( + ["browser/preferences/preferences.ftl", "branding/brand.ftl"], + false, + undefined, + locales + ); +} + +var gNodeToObjectMap = new WeakMap(); + +var gMainPane = { + // The set of types the app knows how to handle. A hash of HandlerInfoWrapper + // objects, indexed by type. + _handledTypes: {}, + + // The list of types we can show, sorted by the sort column/direction. + // An array of HandlerInfoWrapper objects. We build this list when we first + // load the data and then rebuild it when users change a pref that affects + // what types we can show or change the sort column/direction. + // Note: this isn't necessarily the list of types we *will* show; if the user + // provides a filter string, we'll only show the subset of types in this list + // that match that string. + _visibleTypes: [], + + // browser.startup.page values + STARTUP_PREF_BLANK: 0, + STARTUP_PREF_HOMEPAGE: 1, + STARTUP_PREF_RESTORE_SESSION: 3, + + // Convenience & Performance Shortcuts + + get _list() { + delete this._list; + return (this._list = document.getElementById("handlersView")); + }, + + get _filter() { + delete this._filter; + return (this._filter = document.getElementById("filter")); + }, + + _backoffIndex: 0, + + /** + * Initialization of gMainPane. + */ + init() { + function setEventListener(aId, aEventType, aCallback) { + document + .getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gMainPane)); + } + + if (AppConstants.HAVE_SHELL_SERVICE) { + this.updateSetDefaultBrowser(); + let win = Services.wm.getMostRecentWindow("navigator:browser"); + + // Exponential backoff mechanism will delay the polling times if user doesn't + // trigger SetDefaultBrowser for a long time. + let backoffTimes = [ + 1000, 1000, 1000, 1000, 2000, 2000, 2000, 5000, 5000, 10000, + ]; + + let pollForDefaultBrowser = () => { + let uri = win.gBrowser.currentURI.spec; + + if ( + (uri == "about:preferences" || uri == "about:preferences#general") && + document.visibilityState == "visible" + ) { + this.updateSetDefaultBrowser(); + } + + // approximately a "requestIdleInterval" + window.setTimeout(() => { + window.requestIdleCallback(pollForDefaultBrowser); + }, backoffTimes[this._backoffIndex + 1 < backoffTimes.length ? this._backoffIndex++ : backoffTimes.length - 1]); + }; + + window.setTimeout(() => { + window.requestIdleCallback(pollForDefaultBrowser); + }, backoffTimes[this._backoffIndex]); + } + + this.initBrowserContainers(); + this.buildContentProcessCountMenuList(); + + this.updateDefaultPerformanceSettingsPref(); + + let defaultPerformancePref = Preferences.get( + "browser.preferences.defaultPerformanceSettings.enabled" + ); + defaultPerformancePref.on("change", () => { + this.updatePerformanceSettingsBox({ duringChangeEvent: true }); + }); + this.updatePerformanceSettingsBox({ duringChangeEvent: false }); + this.displayUseSystemLocale(); + this.updateProxySettingsUI(); + initializeProxyUI(gMainPane); + + if (Services.prefs.getBoolPref("intl.multilingual.enabled")) { + gMainPane.initPrimaryBrowserLanguageUI(); + } + + // We call `initDefaultZoomValues` to set and unhide the + // default zoom preferences menu, and to establish a + // listener for future menu changes. + gMainPane.initDefaultZoomValues(); + + gMainPane.initTranslations(); + + if ( + Services.prefs.getBoolPref( + "media.videocontrols.picture-in-picture.enabled" + ) + ) { + document.getElementById("pictureInPictureBox").hidden = false; + setEventListener( + "pictureInPictureToggleEnabled", + "command", + function (event) { + if (!event.target.checked) { + Services.telemetry.recordEvent( + "pictureinpicture.settings", + "disable", + "settings" + ); + } + } + ); + } + + if (AppConstants.platform == "win") { + // Functionality for "Show tabs in taskbar" on Windows 7 and up. + try { + let ver = parseFloat(Services.sysinfo.getProperty("version")); + let showTabsInTaskbar = document.getElementById("showTabsInTaskbar"); + showTabsInTaskbar.hidden = ver < 6.1; + } catch (ex) {} + } + + // The "opening multiple tabs might slow down Firefox" warning provides + // an option for not showing this warning again. When the user disables it, + // we provide checkboxes to re-enable the warning. + if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnOpen")) { + document.getElementById("warnOpenMany").hidden = true; + } + + if (AppConstants.platform != "win") { + let quitKeyElement = + window.browsingContext.topChromeWindow.document.getElementById( + "key_quitApplication" + ); + if (quitKeyElement) { + let quitKey = ShortcutUtils.prettifyShortcut(quitKeyElement); + document.l10n.setAttributes( + document.getElementById("warnOnQuitKey"), + "confirm-on-quit-with-key", + { quitKey } + ); + } else { + // If the quit key element does not exist, then the quit key has + // been disabled, so just hide the checkbox. + document.getElementById("warnOnQuitKey").hidden = true; + } + } + + setEventListener("ctrlTabRecentlyUsedOrder", "command", function () { + Services.prefs.clearUserPref("browser.ctrlTab.migrated"); + }); + setEventListener("manageBrowserLanguagesButton", "command", function () { + gMainPane.showBrowserLanguagesSubDialog({ search: false }); + }); + if (AppConstants.MOZ_UPDATER) { + // These elements are only compiled in when the updater is enabled + setEventListener("checkForUpdatesButton", "command", function () { + gAppUpdater.checkForUpdates(); + }); + setEventListener("downloadAndInstallButton", "command", function () { + gAppUpdater.startDownload(); + }); + setEventListener("updateButton", "command", function () { + gAppUpdater.buttonRestartAfterDownload(); + }); + setEventListener("checkForUpdatesButton2", "command", function () { + gAppUpdater.checkForUpdates(); + }); + setEventListener("checkForUpdatesButton3", "command", function () { + gAppUpdater.checkForUpdates(); + }); + setEventListener("checkForUpdatesButton4", "command", function () { + gAppUpdater.checkForUpdates(); + }); + } + + // Startup pref + setEventListener( + "browserRestoreSession", + "command", + gMainPane.onBrowserRestoreSessionChange + ); + gMainPane.updateBrowserStartupUI = + gMainPane.updateBrowserStartupUI.bind(gMainPane); + Preferences.get("browser.privatebrowsing.autostart").on( + "change", + gMainPane.updateBrowserStartupUI + ); + Preferences.get("browser.startup.page").on( + "change", + gMainPane.updateBrowserStartupUI + ); + Preferences.get("browser.startup.homepage").on( + "change", + gMainPane.updateBrowserStartupUI + ); + gMainPane.updateBrowserStartupUI(); + + if (AppConstants.HAVE_SHELL_SERVICE) { + setEventListener( + "setDefaultButton", + "command", + gMainPane.setDefaultBrowser + ); + } + setEventListener( + "disableContainersExtension", + "command", + makeDisableControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY) + ); + setEventListener("chooseLanguage", "command", gMainPane.showLanguages); + setEventListener( + "translationAttributionImage", + "click", + gMainPane.openTranslationProviderAttribution + ); + // TODO (Bug 1817084) Remove this code when we disable the extension + setEventListener( + "translateButton", + "command", + gMainPane.showTranslationExceptions + ); + // TODO (Bug 1817084) Remove this code when we disable the extension + setEventListener( + "fxtranslateButton", + "command", + gMainPane.showTranslationExceptions + ); + Preferences.get("font.language.group").on( + "change", + gMainPane._rebuildFonts.bind(gMainPane) + ); + setEventListener("advancedFonts", "command", gMainPane.configureFonts); + setEventListener("colors", "command", gMainPane.configureColors); + Preferences.get("layers.acceleration.disabled").on( + "change", + gMainPane.updateHardwareAcceleration.bind(gMainPane) + ); + setEventListener( + "connectionSettings", + "command", + gMainPane.showConnections + ); + setEventListener( + "browserContainersCheckbox", + "command", + gMainPane.checkBrowserContainers + ); + setEventListener( + "browserContainersSettings", + "command", + gMainPane.showContainerSettings + ); + setEventListener( + "data-migration", + "command", + gMainPane.onMigrationButtonCommand + ); + + document + .getElementById("migrationWizardDialog") + .addEventListener("MigrationWizard:Close", function (e) { + e.currentTarget.close(); + }); + + if (Services.policies && !Services.policies.isAllowed("profileImport")) { + document.getElementById("dataMigrationGroup").remove(); + } + + // For media control toggle button, we support it on Windows 8.1+ (NT6.3), + // MacOs 10.4+ (darwin8.0, but we already don't support that) and + // gtk-based Linux. + if ( + AppConstants.isPlatformAndVersionAtLeast("win", "6.3") || + AppConstants.platform == "macosx" || + AppConstants.MOZ_WIDGET_GTK + ) { + document.getElementById("mediaControlBox").hidden = false; + } + + // Initializes the fonts dropdowns displayed in this pane. + this._rebuildFonts(); + + this.updateOnScreenKeyboardVisibility(); + + // Show translation preferences if we may: + const translationsPrefName = "browser.translation.ui.show"; + if (Services.prefs.getBoolPref(translationsPrefName)) { + let row = document.getElementById("translationBox"); + row.removeAttribute("hidden"); + // Showing attribution only for Bing Translator. + var { Translation } = ChromeUtils.import( + "resource:///modules/translation/TranslationParent.jsm" + ); + if (Translation.translationEngine == "Bing") { + document.getElementById("bingAttribution").removeAttribute("hidden"); + } + } + + // Firefox Translations settings panel + // TODO (Bug 1817084) Remove this code when we disable the extension + const fxtranslationsDisabledPrefName = "extensions.translations.disabled"; + if (!Services.prefs.getBoolPref(fxtranslationsDisabledPrefName, true)) { + let fxtranslationRow = document.getElementById("fxtranslationsBox"); + fxtranslationRow.hidden = false; + } + + let emeUIEnabled = Services.prefs.getBoolPref("browser.eme.ui.enabled"); + // Force-disable/hide on WinXP: + if (navigator.platform.toLowerCase().startsWith("win")) { + emeUIEnabled = + emeUIEnabled && parseFloat(Services.sysinfo.get("version")) >= 6; + } + if (!emeUIEnabled) { + // Don't want to rely on .hidden for the toplevel groupbox because + // of the pane hiding/showing code potentially interfering: + document + .getElementById("drmGroup") + .setAttribute("style", "display: none !important"); + } + // Initialize the Firefox Updates section. + let version = AppConstants.MOZ_APP_VERSION_DISPLAY; + + // Include the build ID if this is an "a#" (nightly) build + if (/a\d+$/.test(version)) { + let buildID = Services.appinfo.appBuildID; + let year = buildID.slice(0, 4); + let month = buildID.slice(4, 6); + let day = buildID.slice(6, 8); + version += ` (${year}-${month}-${day})`; + } + + // Append "(32-bit)" or "(64-bit)" build architecture to the version number: + let bundle = Services.strings.createBundle( + "chrome://browser/locale/browser.properties" + ); + let archResource = Services.appinfo.is64Bit + ? "aboutDialog.architecture.sixtyFourBit" + : "aboutDialog.architecture.thirtyTwoBit"; + let arch = bundle.GetStringFromName(archResource); + version += ` (${arch})`; + + document.l10n.setAttributes( + document.getElementById("updateAppInfo"), + "update-application-version", + { version } + ); + + // Show a release notes link if we have a URL. + let relNotesLink = document.getElementById("releasenotes"); + let relNotesPrefType = Services.prefs.getPrefType("app.releaseNotesURL"); + if (relNotesPrefType != Services.prefs.PREF_INVALID) { + let relNotesURL = Services.urlFormatter.formatURLPref( + "app.releaseNotesURL" + ); + if (relNotesURL != "about:blank") { + relNotesLink.href = relNotesURL; + relNotesLink.hidden = false; + } + } + + let defaults = Services.prefs.getDefaultBranch(null); + let distroId = defaults.getCharPref("distribution.id", ""); + if (distroId) { + let distroString = distroId; + + let distroVersion = defaults.getCharPref("distribution.version", ""); + if (distroVersion) { + distroString += " - " + distroVersion; + } + + let distroIdField = document.getElementById("distributionId"); + distroIdField.value = distroString; + distroIdField.hidden = false; + + let distroAbout = defaults.getStringPref("distribution.about", ""); + if (distroAbout) { + let distroField = document.getElementById("distribution"); + distroField.value = distroAbout; + distroField.hidden = false; + } + } + + if (AppConstants.MOZ_UPDATER) { + gAppUpdater = new appUpdater(); + setEventListener("showUpdateHistory", "command", gMainPane.showUpdates); + + let updateDisabled = + Services.policies && !Services.policies.isAllowed("appUpdate"); + + if (gIsPackagedApp) { + // When we're running inside an app package, there's no point in + // displaying any update content here, and it would get confusing if we + // did, because our updater is not enabled. + // We can't rely on the hidden attribute for the toplevel elements, + // because of the pane hiding/showing code interfering. + document + .getElementById("updatesCategory") + .setAttribute("style", "display: none !important"); + document + .getElementById("updateApp") + .setAttribute("style", "display: none !important"); + } else if ( + updateDisabled || + UpdateUtils.appUpdateAutoSettingIsLocked() || + gApplicationUpdateService.manualUpdateOnly + ) { + document.getElementById("updateAllowDescription").hidden = true; + document.getElementById("updateSettingsContainer").hidden = true; + if (updateDisabled && AppConstants.MOZ_MAINTENANCE_SERVICE) { + document.getElementById("useService").hidden = true; + } + } else { + // Start with no option selected since we are still reading the value + document.getElementById("autoDesktop").removeAttribute("selected"); + document.getElementById("manualDesktop").removeAttribute("selected"); + // Start reading the correct value from the disk + this.readUpdateAutoPref(); + setEventListener("updateRadioGroup", "command", event => { + if (event.target.id == "backgroundUpdate") { + this.writeBackgroundUpdatePref(); + } else { + this.writeUpdateAutoPref(); + } + }); + if (this.isBackgroundUpdateUIAvailable()) { + document.getElementById("backgroundUpdate").hidden = false; + // Start reading the background update pref's value from the disk. + this.readBackgroundUpdatePref(); + } + } + + if (AppConstants.platform == "win") { + // On Windows, the Application Update setting is an installation- + // specific preference, not a profile-specific one. Show a warning to + // inform users of this. + let updateContainer = document.getElementById( + "updateSettingsContainer" + ); + updateContainer.classList.add("updateSettingCrossUserWarningContainer"); + document.getElementById( + "updateSettingCrossUserWarningDesc" + ).hidden = false; + } + + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + // Check to see if the maintenance service is installed. + // If it isn't installed, don't show the preference at all. + let installed; + try { + let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( + Ci.nsIWindowsRegKey + ); + wrk.open( + wrk.ROOT_KEY_LOCAL_MACHINE, + "SOFTWARE\\Mozilla\\MaintenanceService", + wrk.ACCESS_READ | wrk.WOW64_64 + ); + installed = wrk.readIntValue("Installed"); + wrk.close(); + } catch (e) {} + if (installed != 1) { + document.getElementById("useService").hidden = true; + } + } + } + + // Initilize Application section. + + // Observe preferences that influence what we display so we can rebuild + // the view when they change. + Services.obs.addObserver(this, AUTO_UPDATE_CHANGED_TOPIC); + Services.obs.addObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC); + + setEventListener("filter", "command", gMainPane.filter); + setEventListener("typeColumn", "click", gMainPane.sort); + setEventListener("actionColumn", "click", gMainPane.sort); + setEventListener("chooseFolder", "command", gMainPane.chooseFolder); + Preferences.get("browser.download.folderList").on( + "change", + gMainPane.displayDownloadDirPref.bind(gMainPane) + ); + Preferences.get("browser.download.dir").on( + "change", + gMainPane.displayDownloadDirPref.bind(gMainPane) + ); + gMainPane.displayDownloadDirPref(); + + // Listen for window unload so we can remove our preference observers. + window.addEventListener("unload", this); + + // Figure out how we should be sorting the list. We persist sort settings + // across sessions, so we can't assume the default sort column/direction. + // XXX should we be using the XUL sort service instead? + if (document.getElementById("actionColumn").hasAttribute("sortDirection")) { + this._sortColumn = document.getElementById("actionColumn"); + // The typeColumn element always has a sortDirection attribute, + // either because it was persisted or because the default value + // from the xul file was used. If we are sorting on the other + // column, we should remove it. + document.getElementById("typeColumn").removeAttribute("sortDirection"); + } else { + this._sortColumn = document.getElementById("typeColumn"); + } + + let browserBundle = document.getElementById("browserBundle"); + appendSearchKeywords("browserContainersSettings", [ + browserBundle.getString("userContextPersonal.label"), + browserBundle.getString("userContextWork.label"), + browserBundle.getString("userContextBanking.label"), + browserBundle.getString("userContextShopping.label"), + ]); + + AppearanceChooser.init(); + + // Notify observers that the UI is now ready + Services.obs.notifyObservers(window, "main-pane-loaded"); + + Preferences.addSyncFromPrefListener( + document.getElementById("defaultFont"), + element => FontBuilder.readFontSelection(element) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("translate"), + () => + this.updateButtons( + "translateButton", + "browser.translation.detectLanguage" + ) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("checkSpelling"), + () => this.readCheckSpelling() + ); + Preferences.addSyncToPrefListener( + document.getElementById("checkSpelling"), + () => this.writeCheckSpelling() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("alwaysAsk"), + () => this.readUseDownloadDir() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("linkTargeting"), + () => this.readLinkTarget() + ); + Preferences.addSyncToPrefListener( + document.getElementById("linkTargeting"), + () => this.writeLinkTarget() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("browserContainersCheckbox"), + () => this.readBrowserContainersCheckbox() + ); + + this.setInitialized(); + }, + + preInit() { + promiseLoadHandlersList = new Promise((resolve, reject) => { + // Load the data and build the list of handlers for applications pane. + // By doing this after pageshow, we ensure it doesn't delay painting + // of the preferences page. + window.addEventListener( + "pageshow", + async () => { + await this.initialized; + try { + this._initListEventHandlers(); + this._loadData(); + await this._rebuildVisibleTypes(); + await this._rebuildView(); + await this._sortListView(); + resolve(); + } catch (ex) { + reject(ex); + } + }, + { once: true } + ); + }); + }, + + handleSubcategory(subcategory) { + if (Services.policies && !Services.policies.isAllowed("profileImport")) { + return false; + } + if (subcategory == "migrate") { + this.showMigrationWizardDialog(); + return true; + } + + if (subcategory == "migrate-autoclose") { + this.showMigrationWizardDialog({ closeTabWhenDone: true }); + } + + return false; + }, + + // CONTAINERS + + /* + * preferences: + * + * privacy.userContext.enabled + * - true if containers is enabled + */ + + /** + * Enables/disables the Settings button used to configure containers + */ + readBrowserContainersCheckbox() { + const pref = Preferences.get("privacy.userContext.enabled"); + const settings = document.getElementById("browserContainersSettings"); + + settings.disabled = !pref.value; + const containersEnabled = Services.prefs.getBoolPref( + "privacy.userContext.enabled" + ); + const containersCheckbox = document.getElementById( + "browserContainersCheckbox" + ); + containersCheckbox.checked = containersEnabled; + handleControllingExtension(PREF_SETTING_TYPE, CONTAINERS_KEY).then( + isControlled => { + containersCheckbox.disabled = isControlled; + } + ); + }, + + /** + * Show the Containers UI depending on the privacy.userContext.ui.enabled pref. + */ + initBrowserContainers() { + if (!Services.prefs.getBoolPref("privacy.userContext.ui.enabled")) { + // The browserContainersGroup element has its own internal padding that + // is visible even if the browserContainersbox is visible, so hide the whole + // groupbox if the feature is disabled to prevent a gap in the preferences. + document + .getElementById("browserContainersbox") + .setAttribute("data-hidden-from-search", "true"); + return; + } + Services.prefs.addObserver(PREF_CONTAINERS_EXTENSION, this); + + document.getElementById("browserContainersbox").hidden = false; + this.readBrowserContainersCheckbox(); + }, + + async onGetStarted(aEvent) { + if (!AppConstants.MOZ_DEV_EDITION) { + return; + } + const win = Services.wm.getMostRecentWindow("navigator:browser"); + if (!win) { + return; + } + const user = await fxAccounts.getSignedInUser(); + if (user) { + // We have a user, open Sync preferences in the same tab + win.openTrustedLinkIn("about:preferences#sync", "current"); + return; + } + if (!(await FxAccounts.canConnectAccount())) { + return; + } + let url = await FxAccounts.config.promiseConnectAccountURI( + "dev-edition-setup" + ); + let accountsTab = win.gBrowser.addWebTab(url); + win.gBrowser.selectedTab = accountsTab; + }, + + // HOME PAGE + /* + * Preferences: + * + * browser.startup.page + * - what page(s) to show when the user starts the application, as an integer: + * + * 0: a blank page (DEPRECATED - this can be set via browser.startup.homepage) + * 1: the home page (as set by the browser.startup.homepage pref) + * 2: the last page the user visited (DEPRECATED) + * 3: windows and tabs from the last session (a.k.a. session restore) + * + * The deprecated option is not exposed in UI; however, if the user has it + * selected and doesn't change the UI for this preference, the deprecated + * option is preserved. + */ + + /** + * Utility function to enable/disable the button specified by aButtonID based + * on the value of the Boolean preference specified by aPreferenceID. + */ + updateButtons(aButtonID, aPreferenceID) { + var button = document.getElementById(aButtonID); + var preference = Preferences.get(aPreferenceID); + button.disabled = !preference.value; + return undefined; + }, + + /** + * Hide/show the "Show my windows and tabs from last time" option based + * on the value of the browser.privatebrowsing.autostart pref. + */ + updateBrowserStartupUI() { + const pbAutoStartPref = Preferences.get( + "browser.privatebrowsing.autostart" + ); + const startupPref = Preferences.get("browser.startup.page"); + + let newValue; + let checkbox = document.getElementById("browserRestoreSession"); + checkbox.disabled = pbAutoStartPref.value || startupPref.locked; + newValue = pbAutoStartPref.value + ? false + : startupPref.value === this.STARTUP_PREF_RESTORE_SESSION; + if (checkbox.checked !== newValue) { + checkbox.checked = newValue; + } + }, + /** + * Fetch the existing default zoom value, initialise and unhide + * the preferences menu. This method also establishes a listener + * to ensure handleDefaultZoomChange is called on future menu + * changes. + */ + async initDefaultZoomValues() { + let win = window.browsingContext.topChromeWindow; + let selected = await win.ZoomUI.getGlobalValue(); + let menulist = document.getElementById("defaultZoom"); + + new SelectionChangedMenulist(menulist, event => { + let parsedZoom = parseFloat((event.target.value / 100).toFixed(2)); + gMainPane.handleDefaultZoomChange(parsedZoom); + }); + + setEventListener("zoomText", "command", function () { + win.ZoomManager.toggleZoom(); + }); + + let zoomValues = win.ZoomManager.zoomValues.map(a => { + return Math.round(a * 100); + }); + + let fragment = document.createDocumentFragment(); + for (let zoomLevel of zoomValues) { + let menuitem = document.createXULElement("menuitem"); + document.l10n.setAttributes(menuitem, "preferences-default-zoom-value", { + percentage: zoomLevel, + }); + menuitem.setAttribute("value", zoomLevel); + fragment.appendChild(menuitem); + } + + let menupopup = menulist.querySelector("menupopup"); + menupopup.appendChild(fragment); + menulist.value = Math.round(selected * 100); + + let checkbox = document.getElementById("zoomText"); + checkbox.checked = !win.ZoomManager.useFullZoom; + + document.getElementById("zoomBox").hidden = false; + }, + + /** + * Initialize the translations view. + */ + async initTranslations() { + if (!Services.prefs.getBoolPref("browser.translations.enable")) { + return; + } + + /** + * Which phase a language download is in. + * + * @typedef {"downloaded" | "loading" | "uninstalled"} DownloadPhase + */ + + // Immediately show the group so that the async load of the component does + // not cause the layout to jump. The group will be empty initially. + document.getElementById("translationsGroup").hidden = false; + + class TranslationsState { + /** + * The fully initialized state. + * + * @param {TranslationsActor} translationsActor + * @param {Object} supportedLanguages + * @param {Array<{ langTag: string, displayName: string}} languageList + * @param {Map<string, DownloadPhase>} downloadPhases + */ + constructor( + translationsActor, + supportedLanguages, + languageList, + downloadPhases + ) { + this.translationsActor = translationsActor; + this.supportedLanguages = supportedLanguages; + this.languageList = languageList; + this.downloadPhases = downloadPhases; + } + + /** + * Handles all of the async initialization logic. + */ + static async create() { + const translationsActor = + window.windowGlobalChild.getActor("Translations"); + const supportedLanguages = + await translationsActor.getSupportedLanguages(); + const languageList = + TranslationsState.getLanguageList(supportedLanguages); + const downloadPhases = await TranslationsState.createDownloadPhases( + translationsActor, + languageList + ); + + if (supportedLanguages.languagePairs.length === 0) { + throw new Error( + "The supported languages list was empty. RemoteSettings may not be available at the moment." + ); + } + + return new TranslationsState( + translationsActor, + supportedLanguages, + languageList, + downloadPhases + ); + } + + /** + * Create a unique list of languages, sorted by the display name. + * + * @param {Object} supportedLanguages + * @returns {Array<{ langTag: string, displayName: string}} + */ + static getLanguageList(supportedLanguages) { + const displayNames = new Map(); + for (const languages of [ + supportedLanguages.fromLanguages, + supportedLanguages.toLanguages, + ]) { + for (const { langTag, displayName } of languages) { + displayNames.set(langTag, displayName); + } + } + + let appLangTag = new Intl.Locale(Services.locale.appLocaleAsBCP47) + .language; + + // Don't offer to download the app's language. + displayNames.delete(appLangTag); + + // Sort the list of languages by the display names. + return [...displayNames.entries()] + .map(([langTag, displayName]) => ({ + langTag, + displayName, + })) + .sort((a, b) => a.displayName.localeCompare(b.displayName)); + } + + /** + * Determine the download phase of each language file. + * + * @param {TranslationsChild} translationsActor + * @param {Array<{ langTag: string, displayName: string}} languageList. + * @returns {Map<string, DownloadPhase>} Map the language tag to whether it is downloaded. + */ + static async createDownloadPhases(translationsActor, languageList) { + const downloadPhases = new Map(); + for (const { langTag } of languageList) { + downloadPhases.set( + langTag, + (await translationsActor.hasAllFilesForLanguage(langTag)) + ? "downloaded" + : "uninstalled" + ); + } + return downloadPhases; + } + } + + class TranslationsView { + /** @type {Map<string, XULButton>} */ + deleteButtons = new Map(); + /** @type {Map<string, XULButton>} */ + downloadButtons = new Map(); + + /** + * @param {TranslationsState} state + */ + constructor(state) { + this.state = state; + this.elements = { + settingsButton: document.getElementById( + "translations-manage-settings-button" + ), + installList: document.getElementById( + "translations-manage-install-list" + ), + installAll: document.getElementById( + "translations-manage-install-all" + ), + deleteAll: document.getElementById("translations-manage-delete-all"), + error: document.getElementById("translations-manage-error"), + }; + this.setup(); + } + + setup() { + this.buildLanguageList(); + + this.elements.settingsButton.addEventListener( + "command", + gMainPane.showTranslationsSettings + ); + this.elements.installAll.addEventListener( + "command", + this.handleInstallAll + ); + this.elements.deleteAll.addEventListener( + "command", + this.handleDeleteAll + ); + } + + handleInstallAll = async () => { + this.hideError(); + this.disableButtons(true); + try { + await this.state.translationsActor.downloadAllFiles(); + this.markAllDownloadPhases("downloaded"); + } catch (error) { + TranslationsView.showError( + "translations-manage-error-download", + error + ); + await this.reloadDownloadPhases(); + this.updateAllButtons(); + } + this.disableButtons(false); + }; + + handleDeleteAll = async () => { + this.hideError(); + this.disableButtons(true); + try { + await this.state.translationsActor.deleteAllLanguageFiles(); + this.markAllDownloadPhases("uninstalled"); + } catch (error) { + TranslationsView.showError("translations-manage-error-delete", error); + // The download phases are invalidated with the error and must be reloaded. + await this.reloadDownloadPhases(); + console.error(error); + } + this.disableButtons(false); + }; + + /** + * @param {string} langTag + * @returns {Function} + */ + getDownloadButtonHandler(langTag) { + return async () => { + this.hideError(); + this.updateDownloadPhase(langTag, "loading"); + try { + await this.state.translationsActor.downloadLanguageFiles(langTag); + this.updateDownloadPhase(langTag, "downloaded"); + } catch (error) { + TranslationsView.showError( + "translations-manage-error-download", + error + ); + this.updateDownloadPhase(langTag, "uninstalled"); + } + }; + } + + /** + * @param {string} langTag + * @returns {Function} + */ + getDeleteButtonHandler(langTag) { + return async () => { + this.hideError(); + this.updateDownloadPhase(langTag, "loading"); + try { + await this.state.translationsActor.deleteLanguageFiles(langTag); + this.updateDownloadPhase(langTag, "uninstalled"); + } catch (error) { + TranslationsView.showError( + "translations-manage-error-delete", + error + ); + // The download phases are invalidated with the error and must be reloaded. + await this.reloadDownloadPhases(); + } + }; + } + + buildLanguageList() { + const listFragment = document.createDocumentFragment(); + + for (const { langTag, displayName } of this.state.languageList) { + const hboxRow = document.createXULElement("hbox"); + hboxRow.classList.add("translations-manage-language"); + + const languageLabel = document.createXULElement("label"); + languageLabel.textContent = displayName; // The display name is already localized. + + const downloadButton = document.createXULElement("button"); + const deleteButton = document.createXULElement("button"); + + downloadButton.addEventListener( + "command", + this.getDownloadButtonHandler(langTag) + ); + deleteButton.addEventListener( + "command", + this.getDeleteButtonHandler(langTag) + ); + + document.l10n.setAttributes( + downloadButton, + "translations-manage-download-button" + ); + document.l10n.setAttributes( + deleteButton, + "translations-manage-delete-button" + ); + + downloadButton.hidden = true; + deleteButton.hidden = true; + + this.deleteButtons.set(langTag, deleteButton); + this.downloadButtons.set(langTag, downloadButton); + + hboxRow.appendChild(languageLabel); + hboxRow.appendChild(downloadButton); + hboxRow.appendChild(deleteButton); + listFragment.appendChild(hboxRow); + } + this.updateAllButtons(); + this.elements.installList.appendChild(listFragment); + this.elements.installList.hidden = false; + } + + /** + * Update the DownloadPhase for a single langTag. + * @param {string} langTag + * @param {DownloadPhase} downloadPhase + */ + updateDownloadPhase(langTag, downloadPhase) { + this.state.downloadPhases.set(langTag, downloadPhase); + this.updateButton(langTag, downloadPhase); + this.updateHeaderButtons(); + } + + /** + * Recreates the download map when the state is invalidated. + */ + async reloadDownloadPhases() { + this.state.downloadPhases = + await TranslationsState.createDownloadPhases( + this.state.translationsActor, + this.state.languageList + ); + this.updateAllButtons(); + } + + /** + * Set all the downloads. + * @param {DownloadPhase} downloadPhase + */ + markAllDownloadPhases(downloadPhase) { + const { downloadPhases } = this.state; + for (const key of downloadPhases.keys()) { + downloadPhases.set(key, downloadPhase); + } + this.updateAllButtons(); + } + + /** + * If all languages are downloaded, or no languages are downloaded then + * the visibility of the buttons need to change. + */ + updateHeaderButtons() { + let allDownloaded = true; + let allUninstalled = true; + for (const downloadPhase of this.state.downloadPhases.values()) { + if (downloadPhase === "loading") { + // Don't count loading towards this calculation. + continue; + } + allDownloaded &&= downloadPhase === "downloaded"; + allUninstalled &&= downloadPhase === "uninstalled"; + } + + this.elements.installAll.hidden = allDownloaded; + this.elements.deleteAll.hidden = allUninstalled; + } + + /** + * Update the buttons according to their download state. + */ + updateAllButtons() { + this.updateHeaderButtons(); + for (const [langTag, downloadPhase] of this.state.downloadPhases) { + this.updateButton(langTag, downloadPhase); + } + } + + /** + * @param {string} langTag + * @param {DownloadPhase} downloadPhase + */ + updateButton(langTag, downloadPhase) { + const downloadButton = this.downloadButtons.get(langTag); + const deleteButton = this.deleteButtons.get(langTag); + switch (downloadPhase) { + case "downloaded": + downloadButton.hidden = true; + deleteButton.hidden = false; + downloadButton.removeAttribute("disabled"); + break; + case "uninstalled": + downloadButton.hidden = false; + deleteButton.hidden = true; + downloadButton.removeAttribute("disabled"); + break; + case "loading": + downloadButton.hidden = false; + deleteButton.hidden = true; + downloadButton.setAttribute("disabled", true); + break; + } + } + + /** + * @param {boolean} isDisabled + */ + disableButtons(isDisabled) { + this.elements.installAll.disabled = isDisabled; + this.elements.deleteAll.disabled = isDisabled; + for (const button of this.downloadButtons.values()) { + button.disabled = isDisabled; + } + for (const button of this.deleteButtons.values()) { + button.disabled = isDisabled; + } + } + + /** + * This method is static in case an error happens during the creation of the + * TranslationsState. + * + * @param {string} l10nId + * @param {Error} error + */ + static showError(l10nId, error) { + console.error(error); + const errorMessage = document.getElementById( + "translations-manage-error" + ); + errorMessage.hidden = false; + document.l10n.setAttributes(errorMessage, l10nId); + } + + hideError() { + this.elements.error.hidden = true; + } + } + + TranslationsState.create().then( + state => { + new TranslationsView(state); + }, + error => { + // This error can happen when a user is not connected to the internet, or + // RemoteSettings is down for some reason. + TranslationsView.showError("translations-manage-error-list", error); + } + ); + }, + + initPrimaryBrowserLanguageUI() { + // Enable telemetry. + Services.telemetry.setEventRecordingEnabled( + "intl.ui.browserLanguage", + true + ); + + // This will register the "command" listener. + let menulist = document.getElementById("primaryBrowserLocale"); + new SelectionChangedMenulist(menulist, event => { + gMainPane.onPrimaryBrowserLanguageMenuChange(event); + }); + + gMainPane.updatePrimaryBrowserLanguageUI(Services.locale.appLocaleAsBCP47); + }, + + /** + * Update the available list of locales and select the locale that the user + * is "selecting". This could be the currently requested locale or a locale + * that the user would like to switch to after confirmation. + * + * @param {string} selected - The selected BCP 47 locale. + */ + async updatePrimaryBrowserLanguageUI(selected) { + let available = await LangPackMatcher.getAvailableLocales(); + let localeNames = Services.intl.getLocaleDisplayNames( + undefined, + available, + { preferNative: true } + ); + let locales = available.map((code, i) => ({ code, name: localeNames[i] })); + locales.sort((a, b) => a.name > b.name); + + let fragment = document.createDocumentFragment(); + for (let { code, name } of locales) { + let menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("value", code); + menuitem.setAttribute("label", name); + fragment.appendChild(menuitem); + } + + // Add an option to search for more languages if downloading is supported. + if (Services.prefs.getBoolPref("intl.multilingual.downloadEnabled")) { + let menuitem = document.createXULElement("menuitem"); + menuitem.id = "primaryBrowserLocaleSearch"; + menuitem.setAttribute( + "label", + await document.l10n.formatValue("browser-languages-search") + ); + menuitem.setAttribute("value", "search"); + fragment.appendChild(menuitem); + } + + let menulist = document.getElementById("primaryBrowserLocale"); + let menupopup = menulist.querySelector("menupopup"); + menupopup.textContent = ""; + menupopup.appendChild(fragment); + menulist.value = selected; + + document.getElementById("browserLanguagesBox").hidden = false; + }, + + /* Show the confirmation message bar to allow a restart into the new locales. */ + async showConfirmLanguageChangeMessageBar(locales) { + let messageBar = document.getElementById("confirmBrowserLanguage"); + + // Get the bundle for the new locale. + let newBundle = getBundleForLocales(locales); + + // Find the messages and labels. + let messages = await Promise.all( + [newBundle, document.l10n].map(async bundle => + bundle.formatValue("confirm-browser-language-change-description") + ) + ); + let buttonLabels = await Promise.all( + [newBundle, document.l10n].map(async bundle => + bundle.formatValue("confirm-browser-language-change-button") + ) + ); + + // If both the message and label are the same, just include one row. + if (messages[0] == messages[1] && buttonLabels[0] == buttonLabels[1]) { + messages.pop(); + buttonLabels.pop(); + } + + let contentContainer = messageBar.querySelector( + ".message-bar-content-container" + ); + contentContainer.textContent = ""; + + for (let i = 0; i < messages.length; i++) { + let messageContainer = document.createXULElement("hbox"); + messageContainer.classList.add("message-bar-content"); + messageContainer.style.flex = "1 50%"; + messageContainer.setAttribute("align", "center"); + + let description = document.createXULElement("description"); + description.classList.add("message-bar-description"); + + if (i == 0 && Services.intl.getScriptDirection(locales[0]) === "rtl") { + description.classList.add("rtl-locale"); + } + description.setAttribute("flex", "1"); + description.textContent = messages[i]; + messageContainer.appendChild(description); + + let button = document.createXULElement("button"); + button.addEventListener( + "command", + gMainPane.confirmBrowserLanguageChange + ); + button.classList.add("message-bar-button"); + button.setAttribute("locales", locales.join(",")); + button.setAttribute("label", buttonLabels[i]); + messageContainer.appendChild(button); + + contentContainer.appendChild(messageContainer); + } + + messageBar.hidden = false; + gMainPane.selectedLocalesForRestart = locales; + }, + + hideConfirmLanguageChangeMessageBar() { + let messageBar = document.getElementById("confirmBrowserLanguage"); + messageBar.hidden = true; + let contentContainer = messageBar.querySelector( + ".message-bar-content-container" + ); + contentContainer.textContent = ""; + gMainPane.requestingLocales = null; + }, + + /* Confirm the locale change and restart the browser in the new locale. */ + confirmBrowserLanguageChange(event) { + let localesString = (event.target.getAttribute("locales") || "").trim(); + if (!localesString || !localesString.length) { + return; + } + let locales = localesString.split(","); + Services.locale.requestedLocales = locales; + + // Record the change in telemetry before we restart. + gMainPane.recordBrowserLanguagesTelemetry("apply"); + + // Restart with the new locale. + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + if (!cancelQuit.data) { + Services.startup.quit( + Services.startup.eAttemptQuit | Services.startup.eRestart + ); + } + }, + + /* Show or hide the confirm change message bar based on the new locale. */ + onPrimaryBrowserLanguageMenuChange(event) { + let locale = event.target.value; + + if (locale == "search") { + gMainPane.showBrowserLanguagesSubDialog({ search: true }); + return; + } else if (locale == Services.locale.appLocaleAsBCP47) { + this.hideConfirmLanguageChangeMessageBar(); + return; + } + + let newLocales = Array.from( + new Set([locale, ...Services.locale.requestedLocales]).values() + ); + + gMainPane.recordBrowserLanguagesTelemetry("reorder"); + + switch (gMainPane.getLanguageSwitchTransitionType(newLocales)) { + case "requires-restart": + // Prepare to change the locales, as they were different. + gMainPane.showConfirmLanguageChangeMessageBar(newLocales); + gMainPane.updatePrimaryBrowserLanguageUI(newLocales[0]); + break; + case "live-reload": + Services.locale.requestedLocales = newLocales; + gMainPane.updatePrimaryBrowserLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gMainPane.hideConfirmLanguageChangeMessageBar(); + break; + case "locales-match": + // They matched, so we can reset the UI. + gMainPane.updatePrimaryBrowserLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gMainPane.hideConfirmLanguageChangeMessageBar(); + break; + default: + throw new Error("Unhandled transition type."); + } + }, + + /** + * Takes as newZoom a floating point value representing the + * new default zoom. This value should not be a string, and + * should not carry a percentage sign/other localisation + * characteristics. + */ + handleDefaultZoomChange(newZoom) { + let cps2 = Cc["@mozilla.org/content-pref/service;1"].getService( + Ci.nsIContentPrefService2 + ); + let nonPrivateLoadContext = Cu.createLoadContext(); + /* Because our setGlobal function takes in a browsing context, and + * because we want to keep this property consistent across both private + * and non-private contexts, we crate a non-private context and use that + * to set the property, regardless of our actual context. + */ + + let win = window.browsingContext.topChromeWindow; + cps2.setGlobal(win.FullZoom.name, newZoom, nonPrivateLoadContext); + }, + + onBrowserRestoreSessionChange(event) { + const value = event.target.checked; + const startupPref = Preferences.get("browser.startup.page"); + let newValue; + + if (value) { + // We need to restore the blank homepage setting in our other pref + if (startupPref.value === this.STARTUP_PREF_BLANK) { + HomePage.safeSet("about:blank"); + } + newValue = this.STARTUP_PREF_RESTORE_SESSION; + } else { + newValue = this.STARTUP_PREF_HOMEPAGE; + } + startupPref.value = newValue; + }, + + // TABS + + /* + * Preferences: + * + * browser.link.open_newwindow - int + * Determines where links targeting new windows should open. + * Values: + * 1 - Open in the current window or tab. + * 2 - Open in a new window. + * 3 - Open in a new tab in the most recent window. + * browser.tabs.loadInBackground - bool + * True - Whether browser should switch to a new tab opened from a link. + * browser.tabs.warnOnClose - bool + * True - If when closing a window with multiple tabs the user is warned and + * allowed to cancel the action, false to just close the window. + * browser.warnOnQuitShortcut - bool + * True - If the keyboard shortcut (Ctrl/Cmd+Q) is pressed, the user should + * be warned, false to just quit without prompting. + * browser.tabs.warnOnOpen - bool + * True - Whether the user should be warned when trying to open a lot of + * tabs at once (e.g. a large folder of bookmarks), allowing to + * cancel the action. + * browser.taskbar.previews.enable - bool + * True - Tabs are to be shown in Windows 7 taskbar. + * False - Only the window is to be shown in Windows 7 taskbar. + */ + + /** + * Determines where a link which opens a new window will open. + * + * @returns |true| if such links should be opened in new tabs + */ + readLinkTarget() { + var openNewWindow = Preferences.get("browser.link.open_newwindow"); + return openNewWindow.value != 2; + }, + + /** + * Determines where a link which opens a new window will open. + * + * @returns 2 if such links should be opened in new windows, + * 3 if such links should be opened in new tabs + */ + writeLinkTarget() { + var linkTargeting = document.getElementById("linkTargeting"); + return linkTargeting.checked ? 3 : 2; + }, + /* + * Preferences: + * + * browser.shell.checkDefault + * - true if a default-browser check (and prompt to make it so if necessary) + * occurs at startup, false otherwise + */ + + /** + * Show button for setting browser as default browser or information that + * browser is already the default browser. + */ + updateSetDefaultBrowser() { + if (AppConstants.HAVE_SHELL_SERVICE) { + let shellSvc = getShellService(); + let defaultBrowserBox = document.getElementById("defaultBrowserBox"); + let isInFlatpak = gGIOService?.isRunningUnderFlatpak; + // Flatpak does not support setting nor detection of default browser + if (!shellSvc || isInFlatpak) { + defaultBrowserBox.hidden = true; + return; + } + let isDefault = shellSvc.isDefaultBrowser(false, true); + let setDefaultPane = document.getElementById("setDefaultPane"); + setDefaultPane.classList.toggle("is-default", isDefault); + let alwaysCheck = document.getElementById("alwaysCheckDefault"); + let alwaysCheckPref = Preferences.get( + "browser.shell.checkDefaultBrowser" + ); + alwaysCheck.disabled = alwaysCheckPref.locked || isDefault; + } + }, + + /** + * Set browser as the operating system default browser. + */ + setDefaultBrowser() { + if (AppConstants.HAVE_SHELL_SERVICE) { + let alwaysCheckPref = Preferences.get( + "browser.shell.checkDefaultBrowser" + ); + alwaysCheckPref.value = true; + + // Reset exponential backoff delay time in order to do visual update in pollForDefaultBrowser. + this._backoffIndex = 0; + + let shellSvc = getShellService(); + if (!shellSvc) { + return; + } + try { + shellSvc.setDefaultBrowser(true, false); + } catch (ex) { + console.error(ex); + return; + } + + let isDefault = shellSvc.isDefaultBrowser(false, true); + let setDefaultPane = document.getElementById("setDefaultPane"); + setDefaultPane.classList.toggle("is-default", isDefault); + } + }, + + /** + * Shows a dialog in which the preferred language for web content may be set. + */ + showLanguages() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/languages.xhtml" + ); + }, + + recordBrowserLanguagesTelemetry(method, value = null) { + Services.telemetry.recordEvent( + "intl.ui.browserLanguage", + method, + "main", + value + ); + }, + + /** + * Open the browser languages sub dialog in either the normal mode, or search mode. + * The search mode is only available from the menu to change the primary browser + * language. + * + * @param {{ search: boolean }} + */ + showBrowserLanguagesSubDialog({ search }) { + // Record the telemetry event with an id to associate related actions. + let telemetryId = parseInt( + Services.telemetry.msSinceProcessStart(), + 10 + ).toString(); + let method = search ? "search" : "manage"; + gMainPane.recordBrowserLanguagesTelemetry(method, telemetryId); + + let opts = { + selectedLocalesForRestart: gMainPane.selectedLocalesForRestart, + search, + telemetryId, + }; + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/browserLanguages.xhtml", + { closingCallback: this.browserLanguagesClosed }, + opts + ); + }, + + /** + * Determine the transition strategy for switching the locale based on prefs + * and the switched locales. + * + * @param {Array<string>} newLocales - List of BCP 47 locale identifiers. + * @returns {"locales-match" | "requires-restart" | "live-reload"} + */ + getLanguageSwitchTransitionType(newLocales) { + const { appLocalesAsBCP47 } = Services.locale; + if (appLocalesAsBCP47.join(",") === newLocales.join(",")) { + // The selected locales match, the order matters. + return "locales-match"; + } + + if (Services.prefs.getBoolPref("intl.multilingual.liveReload")) { + if ( + Services.intl.getScriptDirection(newLocales[0]) !== + Services.intl.getScriptDirection(appLocalesAsBCP47[0]) && + !Services.prefs.getBoolPref("intl.multilingual.liveReloadBidirectional") + ) { + // Bug 1750852: The directionality of the text changed, which requires a restart + // until the quality of the switch can be improved. + return "requires-restart"; + } + + return "live-reload"; + } + + return "requires-restart"; + }, + + /* Show or hide the confirm change message bar based on the updated ordering. */ + browserLanguagesClosed() { + // When the subdialog is closed, settings are stored on gBrowserLanguagesDialog. + // The next time the dialog is opened, a new gBrowserLanguagesDialog is created. + let { selected } = this.gBrowserLanguagesDialog; + + this.gBrowserLanguagesDialog.recordTelemetry( + selected ? "accept" : "cancel" + ); + + if (!selected) { + // No locales were selected. Cancel the operation. + return; + } + + switch (gMainPane.getLanguageSwitchTransitionType(selected)) { + case "requires-restart": + gMainPane.showConfirmLanguageChangeMessageBar(selected); + gMainPane.updatePrimaryBrowserLanguageUI(selected[0]); + break; + case "live-reload": + Services.locale.requestedLocales = selected; + + gMainPane.updatePrimaryBrowserLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gMainPane.hideConfirmLanguageChangeMessageBar(); + break; + case "locales-match": + // They matched, so we can reset the UI. + gMainPane.updatePrimaryBrowserLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gMainPane.hideConfirmLanguageChangeMessageBar(); + break; + default: + throw new Error("Unhandled transition type."); + } + }, + + displayUseSystemLocale() { + let appLocale = Services.locale.appLocaleAsBCP47; + let regionalPrefsLocales = Services.locale.regionalPrefsLocales; + if (!regionalPrefsLocales.length) { + return; + } + let systemLocale = regionalPrefsLocales[0]; + let localeDisplayname = Services.intl.getLocaleDisplayNames( + undefined, + [systemLocale], + { preferNative: true } + ); + if (!localeDisplayname.length) { + return; + } + let localeName = localeDisplayname[0]; + if (appLocale.split("-u-")[0] != systemLocale.split("-u-")[0]) { + let checkbox = document.getElementById("useSystemLocale"); + document.l10n.setAttributes(checkbox, "use-system-locale", { + localeName, + }); + checkbox.hidden = false; + } + }, + + /** + * Displays the translation exceptions dialog where specific site and language + * translation preferences can be set. + */ + // TODO (Bug 1817084) Remove this code when we disable the extension + showTranslationExceptions() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/translationExceptions.xhtml" + ); + }, + + showTranslationsSettings() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/translations.xhtml" + ); + }, + + openTranslationProviderAttribution() { + var { Translation } = ChromeUtils.import( + "resource:///modules/translation/TranslationParent.jsm" + ); + Translation.openProviderAttribution(); + }, + + /** + * Displays the fonts dialog, where web page font names and sizes can be + * configured. + */ + configureFonts() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/fonts.xhtml", + { features: "resizable=no" } + ); + }, + + /** + * Displays the colors dialog, where default web page/link/etc. colors can be + * configured. + */ + configureColors() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/colors.xhtml", + { features: "resizable=no" } + ); + }, + + // NETWORK + /** + * Displays a dialog in which proxy settings may be changed. + */ + showConnections() { + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/connection.xhtml", + { closingCallback: this.updateProxySettingsUI.bind(this) } + ); + }, + + // Update the UI to show the proper description depending on whether an + // extension is in control or not. + async updateProxySettingsUI() { + let controllingExtension = await getControllingExtension( + PREF_SETTING_TYPE, + PROXY_KEY + ); + let description = document.getElementById("connectionSettingsDescription"); + + if (controllingExtension) { + setControllingExtensionDescription( + description, + controllingExtension, + "proxy.settings" + ); + } else { + setControllingExtensionDescription( + description, + null, + "network-proxy-connection-description" + ); + } + }, + + async checkBrowserContainers(event) { + let checkbox = document.getElementById("browserContainersCheckbox"); + if (checkbox.checked) { + Services.prefs.setBoolPref("privacy.userContext.enabled", true); + return; + } + + let count = ContextualIdentityService.countContainerTabs(); + if (count == 0) { + Services.prefs.setBoolPref("privacy.userContext.enabled", false); + return; + } + + let [title, message, okButton, cancelButton] = + await document.l10n.formatValues([ + { id: "containers-disable-alert-title" }, + { id: "containers-disable-alert-desc", args: { tabCount: count } }, + { id: "containers-disable-alert-ok-button", args: { tabCount: count } }, + { id: "containers-disable-alert-cancel-button" }, + ]); + + let buttonFlags = + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1; + + let rv = Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + okButton, + cancelButton, + null, + null, + {} + ); + if (rv == 0) { + Services.prefs.setBoolPref("privacy.userContext.enabled", false); + return; + } + + checkbox.checked = true; + }, + + /** + * Displays container panel for customising and adding containers. + */ + showContainerSettings() { + gotoPref("containers"); + }, + + /** + * ui.osk.enabled + * - when set to true, subject to other conditions, we may sometimes invoke + * an on-screen keyboard when a text input is focused. + * (Currently Windows-only, and depending on prefs, may be Windows-8-only) + */ + updateOnScreenKeyboardVisibility() { + if (AppConstants.platform == "win") { + let minVersion = Services.prefs.getBoolPref("ui.osk.require_win10") + ? 10 + : 6.2; + if ( + Services.vc.compare( + Services.sysinfo.getProperty("version"), + minVersion + ) >= 0 + ) { + document.getElementById("useOnScreenKeyboard").hidden = false; + } + } + }, + + updateHardwareAcceleration() { + // Placeholder for restart on change + }, + + // FONTS + + /** + * Populates the default font list in UI. + */ + _rebuildFonts() { + var langGroupPref = Preferences.get("font.language.group"); + var isSerif = + this._readDefaultFontTypeForLanguage(langGroupPref.value) == "serif"; + this._selectDefaultLanguageGroup(langGroupPref.value, isSerif); + }, + + /** + * Returns the type of the current default font for the language denoted by + * aLanguageGroup. + */ + _readDefaultFontTypeForLanguage(aLanguageGroup) { + const kDefaultFontType = "font.default.%LANG%"; + var defaultFontTypePref = kDefaultFontType.replace( + /%LANG%/, + aLanguageGroup + ); + var preference = Preferences.get(defaultFontTypePref); + if (!preference) { + preference = Preferences.add({ id: defaultFontTypePref, type: "string" }); + preference.on("change", gMainPane._rebuildFonts.bind(gMainPane)); + } + return preference.value; + }, + + _selectDefaultLanguageGroupPromise: Promise.resolve(), + + _selectDefaultLanguageGroup(aLanguageGroup, aIsSerif) { + this._selectDefaultLanguageGroupPromise = (async () => { + // Avoid overlapping language group selections by awaiting the resolution + // of the previous one. We do this because this function is re-entrant, + // as inserting <preference> elements into the DOM sometimes triggers a call + // back into this function. And since this function is also asynchronous, + // that call can enter this function before the previous run has completed, + // which would corrupt the font menulists. Awaiting the previous call's + // resolution avoids that fate. + await this._selectDefaultLanguageGroupPromise; + + const kFontNameFmtSerif = "font.name.serif.%LANG%"; + const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%"; + const kFontNameListFmtSerif = "font.name-list.serif.%LANG%"; + const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%"; + const kFontSizeFmtVariable = "font.size.variable.%LANG%"; + + var prefs = [ + { + format: aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif, + type: "fontname", + element: "defaultFont", + fonttype: aIsSerif ? "serif" : "sans-serif", + }, + { + format: aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif, + type: "unichar", + element: null, + fonttype: aIsSerif ? "serif" : "sans-serif", + }, + { + format: kFontSizeFmtVariable, + type: "int", + element: "defaultFontSize", + fonttype: null, + }, + ]; + for (var i = 0; i < prefs.length; ++i) { + var preference = Preferences.get( + prefs[i].format.replace(/%LANG%/, aLanguageGroup) + ); + if (!preference) { + var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup); + preference = Preferences.add({ id: name, type: prefs[i].type }); + } + + if (!prefs[i].element) { + continue; + } + + var element = document.getElementById(prefs[i].element); + if (element) { + element.setAttribute("preference", preference.id); + + if (prefs[i].fonttype) { + await FontBuilder.buildFontList( + aLanguageGroup, + prefs[i].fonttype, + element + ); + } + + preference.setElementValue(element); + } + } + })().catch(console.error); + }, + + onMigrationButtonCommand(command) { + // When browser.migrate.content-modal.enabled is enabled by default, + // the event handler can just call showMigrationWizardDialog directly, + // but for now, we delegate to MigrationUtils to open the native modal + // in case that's the dialog we're still using. + // + // Enabling the pref by default will be part of bug 1822156. + const browser = window.docShell.chromeEventHandler; + const browserWindow = browser.ownerGlobal; + + // showMigrationWizard blocks on some platforms. We'll dispatch the request + // to open to a runnable on the main thread so that we don't have to block + // this function call. + Services.tm.dispatchToMainThread(() => { + MigrationUtils.showMigrationWizard(browserWindow, { + entrypoint: MigrationUtils.MIGRATION_ENTRYPOINTS.PREFERENCES, + }); + }); + }, + + /** + * Displays the migration wizard dialog in an HTML dialog. + */ + async showMigrationWizardDialog({ closeTabWhenDone = false } = {}) { + let migrationWizardDialog = document.getElementById( + "migrationWizardDialog" + ); + + if (migrationWizardDialog.open) { + return; + } + + await customElements.whenDefined("migration-wizard"); + + // If we've been opened before, remove the old wizard and insert a + // new one to put it back into its starting state. + if (!migrationWizardDialog.firstElementChild) { + let wizard = document.createElement("migration-wizard"); + wizard.toggleAttribute("dialog-mode", true); + + let panelList = document.createElement("panel-list"); + let panel = document.createXULElement("panel"); + panel.appendChild(panelList); + wizard.appendChild(panel); + + migrationWizardDialog.appendChild(wizard); + } + migrationWizardDialog.firstElementChild.requestState(); + + migrationWizardDialog.addEventListener( + "close", + () => { + // Let others know that the wizard is closed -- potentially because of a + // user action within the dialog that dispatches "MigrationWizard:Close" + // but this also covers cases like hitting Escape. + Services.obs.notifyObservers( + migrationWizardDialog, + "MigrationWizard:Closed" + ); + if (closeTabWhenDone) { + window.close(); + } + }, + { once: true } + ); + + migrationWizardDialog.showModal(); + }, + + /** + * Stores the original value of the spellchecking preference to enable proper + * restoration if unchanged (since we're mapping a tristate onto a checkbox). + */ + _storedSpellCheck: 0, + + /** + * Returns true if any spellchecking is enabled and false otherwise, caching + * the current value to enable proper pref restoration if the checkbox is + * never changed. + * + * layout.spellcheckDefault + * - an integer: + * 0 disables spellchecking + * 1 enables spellchecking, but only for multiline text fields + * 2 enables spellchecking for all text fields + */ + readCheckSpelling() { + var pref = Preferences.get("layout.spellcheckDefault"); + this._storedSpellCheck = pref.value; + + return pref.value != 0; + }, + + /** + * Returns the value of the spellchecking preference represented by UI, + * preserving the preference's "hidden" value if the preference is + * unchanged and represents a value not strictly allowed in UI. + */ + writeCheckSpelling() { + var checkbox = document.getElementById("checkSpelling"); + if (checkbox.checked) { + if (this._storedSpellCheck == 2) { + return 2; + } + return 1; + } + return 0; + }, + + updateDefaultPerformanceSettingsPref() { + let defaultPerformancePref = Preferences.get( + "browser.preferences.defaultPerformanceSettings.enabled" + ); + let processCountPref = Preferences.get("dom.ipc.processCount"); + let accelerationPref = Preferences.get("layers.acceleration.disabled"); + if ( + processCountPref.value != processCountPref.defaultValue || + accelerationPref.value != accelerationPref.defaultValue + ) { + defaultPerformancePref.value = false; + } + }, + + updatePerformanceSettingsBox({ duringChangeEvent }) { + let defaultPerformancePref = Preferences.get( + "browser.preferences.defaultPerformanceSettings.enabled" + ); + let performanceSettings = document.getElementById("performanceSettings"); + let processCountPref = Preferences.get("dom.ipc.processCount"); + if (defaultPerformancePref.value) { + let accelerationPref = Preferences.get("layers.acceleration.disabled"); + // Unset the value so process count will be decided by the platform. + processCountPref.value = processCountPref.defaultValue; + accelerationPref.value = accelerationPref.defaultValue; + performanceSettings.hidden = true; + } else { + performanceSettings.hidden = false; + } + }, + + buildContentProcessCountMenuList() { + if (Services.appinfo.fissionAutostart) { + document.getElementById("limitContentProcess").hidden = true; + document.getElementById("contentProcessCount").hidden = true; + document.getElementById( + "contentProcessCountEnabledDescription" + ).hidden = true; + document.getElementById( + "contentProcessCountDisabledDescription" + ).hidden = true; + return; + } + if (Services.appinfo.browserTabsRemoteAutostart) { + let processCountPref = Preferences.get("dom.ipc.processCount"); + let defaultProcessCount = processCountPref.defaultValue; + + let contentProcessCount = + document.querySelector(`#contentProcessCount > menupopup > + menuitem[value="${defaultProcessCount}"]`); + + document.l10n.setAttributes( + contentProcessCount, + "performance-default-content-process-count", + { num: defaultProcessCount } + ); + + document.getElementById("limitContentProcess").disabled = false; + document.getElementById("contentProcessCount").disabled = false; + document.getElementById( + "contentProcessCountEnabledDescription" + ).hidden = false; + document.getElementById( + "contentProcessCountDisabledDescription" + ).hidden = true; + } else { + document.getElementById("limitContentProcess").disabled = true; + document.getElementById("contentProcessCount").disabled = true; + document.getElementById( + "contentProcessCountEnabledDescription" + ).hidden = true; + document.getElementById( + "contentProcessCountDisabledDescription" + ).hidden = false; + } + }, + + _minUpdatePrefDisableTime: 1000, + /** + * Selects the correct item in the update radio group + */ + async readUpdateAutoPref() { + if ( + AppConstants.MOZ_UPDATER && + (!Services.policies || Services.policies.isAllowed("appUpdate")) && + !gIsPackagedApp + ) { + let radiogroup = document.getElementById("updateRadioGroup"); + + radiogroup.disabled = true; + let enabled = await UpdateUtils.getAppUpdateAutoEnabled(); + radiogroup.value = enabled; + radiogroup.disabled = false; + + this.maybeDisableBackgroundUpdateControls(); + } + }, + + /** + * Writes the value of the automatic update radio group to the disk + */ + async writeUpdateAutoPref() { + if ( + AppConstants.MOZ_UPDATER && + (!Services.policies || Services.policies.isAllowed("appUpdate")) && + !gIsPackagedApp + ) { + let radiogroup = document.getElementById("updateRadioGroup"); + let updateAutoValue = radiogroup.value == "true"; + let _disableTimeOverPromise = new Promise(r => + setTimeout(r, this._minUpdatePrefDisableTime) + ); + radiogroup.disabled = true; + try { + await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue); + await _disableTimeOverPromise; + radiogroup.disabled = false; + } catch (error) { + console.error(error); + await Promise.all([ + this.readUpdateAutoPref(), + this.reportUpdatePrefWriteError(), + ]); + return; + } + + this.maybeDisableBackgroundUpdateControls(); + + // If the value was changed to false the user should be given the option + // to discard an update if there is one. + if (!updateAutoValue) { + await this.checkUpdateInProgress(); + } + // For tests: + radiogroup.dispatchEvent(new CustomEvent("ProcessedUpdatePrefChange")); + } + }, + + isBackgroundUpdateUIAvailable() { + return ( + AppConstants.MOZ_UPDATE_AGENT && + // This UI controls a per-installation pref. It won't necessarily work + // properly if per-installation prefs aren't supported. + UpdateUtils.PER_INSTALLATION_PREFS_SUPPORTED && + (!Services.policies || Services.policies.isAllowed("appUpdate")) && + !gIsPackagedApp && + !UpdateUtils.appUpdateSettingIsLocked("app.update.background.enabled") + ); + }, + + maybeDisableBackgroundUpdateControls() { + if (this.isBackgroundUpdateUIAvailable()) { + let radiogroup = document.getElementById("updateRadioGroup"); + let updateAutoEnabled = radiogroup.value == "true"; + + // This control is only active if auto update is enabled. + document.getElementById("backgroundUpdate").disabled = !updateAutoEnabled; + } + }, + + async readBackgroundUpdatePref() { + const prefName = "app.update.background.enabled"; + if (this.isBackgroundUpdateUIAvailable()) { + let backgroundCheckbox = document.getElementById("backgroundUpdate"); + + // When the page first loads, the checkbox is unchecked until we finish + // reading the config file from the disk. But, ideally, we don't want to + // give the user the impression that this setting has somehow gotten + // turned off and they need to turn it back on. We also don't want the + // user interacting with the control, expecting a particular behavior, and + // then have the read complete and change the control in an unexpected + // way. So we disable the control while we are reading. + // The only entry points for this function are page load and user + // interaction with the control. By disabling the control to prevent + // further user interaction, we prevent the possibility of entering this + // function a second time while we are still reading. + backgroundCheckbox.disabled = true; + + // If we haven't already done this, it might result in the effective value + // of the Background Update pref changing. Thus, we should do it before + // we tell the user what value this pref has. + await BackgroundUpdate.ensureExperimentToRolloutTransitionPerformed(); + + let enabled = await UpdateUtils.readUpdateConfigSetting(prefName); + backgroundCheckbox.checked = enabled; + this.maybeDisableBackgroundUpdateControls(); + } + }, + + async writeBackgroundUpdatePref() { + const prefName = "app.update.background.enabled"; + if (this.isBackgroundUpdateUIAvailable()) { + let backgroundCheckbox = document.getElementById("backgroundUpdate"); + backgroundCheckbox.disabled = true; + let backgroundUpdateEnabled = backgroundCheckbox.checked; + try { + await UpdateUtils.writeUpdateConfigSetting( + prefName, + backgroundUpdateEnabled + ); + } catch (error) { + console.error(error); + await this.readBackgroundUpdatePref(); + await this.reportUpdatePrefWriteError(); + return; + } + + this.maybeDisableBackgroundUpdateControls(); + } + }, + + async reportUpdatePrefWriteError() { + let [title, message] = await document.l10n.formatValues([ + { id: "update-setting-write-failure-title2" }, + { + id: "update-setting-write-failure-message2", + args: { path: UpdateUtils.configFilePath }, + }, + ]); + + // Set up the Ok Button + let buttonFlags = + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK; + Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + null, + null, + null, + null, + {} + ); + }, + + async checkUpdateInProgress() { + let um = Cc["@mozilla.org/updates/update-manager;1"].getService( + Ci.nsIUpdateManager + ); + if (!um.readyUpdate && !um.downloadingUpdate) { + return; + } + + let [title, message, okButton, cancelButton] = + await document.l10n.formatValues([ + { id: "update-in-progress-title" }, + { id: "update-in-progress-message" }, + { id: "update-in-progress-ok-button" }, + { id: "update-in-progress-cancel-button" }, + ]); + + // Continue is the cancel button which is BUTTON_POS_1 and is set as the + // default so pressing escape or using a platform standard method of closing + // the UI will not discard the update. + let buttonFlags = + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 + + Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; + + let rv = Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + okButton, + cancelButton, + null, + null, + {} + ); + if (rv != 1) { + let aus = Cc["@mozilla.org/updates/update-service;1"].getService( + Ci.nsIApplicationUpdateService + ); + await aus.stopDownload(); + um.cleanupReadyUpdate(); + um.cleanupDownloadingUpdate(); + } + }, + + /** + * Displays the history of installed updates. + */ + showUpdates() { + gSubDialog.open("chrome://mozapps/content/update/history.xhtml"); + }, + + destroy() { + window.removeEventListener("unload", this); + Services.prefs.removeObserver(PREF_CONTAINERS_EXTENSION, this); + Services.obs.removeObserver(this, AUTO_UPDATE_CHANGED_TOPIC); + Services.obs.removeObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC); + AppearanceChooser.destroy(); + }, + + // nsISupports + + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + // nsIObserver + + async observe(aSubject, aTopic, aData) { + if (aTopic == "nsPref:changed") { + if (aData == PREF_CONTAINERS_EXTENSION) { + this.readBrowserContainersCheckbox(); + return; + } + // Rebuild the list when there are changes to preferences that influence + // whether or not to show certain entries in the list. + if (!this._storingAction) { + await this._rebuildView(); + } + } else if (aTopic == AUTO_UPDATE_CHANGED_TOPIC) { + if (!AppConstants.MOZ_UPDATER) { + return; + } + if (aData != "true" && aData != "false") { + throw new Error("Invalid preference value for app.update.auto"); + } + document.getElementById("updateRadioGroup").value = aData; + this.maybeDisableBackgroundUpdateControls(); + } else if (aTopic == BACKGROUND_UPDATE_CHANGED_TOPIC) { + if (!AppConstants.MOZ_UPDATE_AGENT) { + return; + } + if (aData != "true" && aData != "false") { + throw new Error( + "Invalid preference value for app.update.background.enabled" + ); + } + document.getElementById("backgroundUpdate").checked = aData == "true"; + } + }, + + // EventListener + + handleEvent(aEvent) { + if (aEvent.type == "unload") { + this.destroy(); + if (AppConstants.MOZ_UPDATER) { + onUnload(); + } + } + }, + + // Composed Model Construction + + _loadData() { + this._loadInternalHandlers(); + this._loadApplicationHandlers(); + }, + + /** + * Load higher level internal handlers so they can be turned on/off in the + * applications menu. + */ + _loadInternalHandlers() { + let internalHandlers = [new PDFHandlerInfoWrapper()]; + + let enabledHandlers = Services.prefs + .getCharPref("browser.download.viewableInternally.enabledTypes", "") + .trim(); + if (enabledHandlers) { + for (let ext of enabledHandlers.split(",")) { + internalHandlers.push( + new ViewableInternallyHandlerInfoWrapper(null, ext.trim()) + ); + } + } + for (let internalHandler of internalHandlers) { + if (internalHandler.enabled) { + this._handledTypes[internalHandler.type] = internalHandler; + } + } + }, + + /** + * Load the set of handlers defined by the application datastore. + */ + _loadApplicationHandlers() { + for (let wrappedHandlerInfo of gHandlerService.enumerate()) { + let type = wrappedHandlerInfo.type; + + let handlerInfoWrapper; + if (type in this._handledTypes) { + handlerInfoWrapper = this._handledTypes[type]; + } else { + if (DownloadIntegration.shouldViewDownloadInternally(type)) { + handlerInfoWrapper = new ViewableInternallyHandlerInfoWrapper(type); + } else { + handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo); + } + this._handledTypes[type] = handlerInfoWrapper; + } + } + }, + + // View Construction + + selectedHandlerListItem: null, + + _initListEventHandlers() { + this._list.addEventListener("select", event => { + if (event.target != this._list) { + return; + } + + let handlerListItem = + this._list.selectedItem && + HandlerListItem.forNode(this._list.selectedItem); + if (this.selectedHandlerListItem == handlerListItem) { + return; + } + + if (this.selectedHandlerListItem) { + this.selectedHandlerListItem.showActionsMenu = false; + } + this.selectedHandlerListItem = handlerListItem; + if (handlerListItem) { + this.rebuildActionsMenu(); + handlerListItem.showActionsMenu = true; + } + }); + }, + + async _rebuildVisibleTypes() { + this._visibleTypes = []; + + // Map whose keys are string descriptions and values are references to the + // first visible HandlerInfoWrapper that has this description. We use this + // to determine whether or not to annotate descriptions with their types to + // distinguish duplicate descriptions from each other. + let visibleDescriptions = new Map(); + for (let type in this._handledTypes) { + // Yield before processing each handler info object to avoid monopolizing + // the main thread, as the objects are retrieved lazily, and retrieval + // can be expensive on Windows. + await new Promise(resolve => Services.tm.dispatchToMainThread(resolve)); + + let handlerInfo = this._handledTypes[type]; + + // We couldn't find any reason to exclude the type, so include it. + this._visibleTypes.push(handlerInfo); + + let key = JSON.stringify(handlerInfo.description); + let otherHandlerInfo = visibleDescriptions.get(key); + if (!otherHandlerInfo) { + // This is the first type with this description that we encountered + // while rebuilding the _visibleTypes array this time. Make sure the + // flag is reset so we won't add the type to the description. + handlerInfo.disambiguateDescription = false; + visibleDescriptions.set(key, handlerInfo); + } else { + // There is at least another type with this description. Make sure we + // add the type to the description on both HandlerInfoWrapper objects. + handlerInfo.disambiguateDescription = true; + otherHandlerInfo.disambiguateDescription = true; + } + } + }, + + async _rebuildView() { + let lastSelectedType = + this.selectedHandlerListItem && + this.selectedHandlerListItem.handlerInfoWrapper.type; + this.selectedHandlerListItem = null; + + // Clear the list of entries. + this._list.textContent = ""; + + var visibleTypes = this._visibleTypes; + + let items = visibleTypes.map( + visibleType => new HandlerListItem(visibleType) + ); + let itemsFragment = document.createDocumentFragment(); + let lastSelectedItem; + for (let item of items) { + item.createNode(itemsFragment); + if (item.handlerInfoWrapper.type == lastSelectedType) { + lastSelectedItem = item; + } + } + + for (let item of items) { + item.setupNode(); + this.rebuildActionsMenu(item.node, item.handlerInfoWrapper); + item.refreshAction(); + } + + // If the user is filtering the list, then only show matching types. + // If we filter, we need to first localize the fragment, to + // be able to filter by localized values. + if (this._filter.value) { + await document.l10n.translateFragment(itemsFragment); + + this._filterView(itemsFragment); + + document.l10n.pauseObserving(); + this._list.appendChild(itemsFragment); + document.l10n.resumeObserving(); + } else { + // Otherwise we can just append the fragment and it'll + // get localized via the Mutation Observer. + this._list.appendChild(itemsFragment); + } + + if (lastSelectedItem) { + this._list.selectedItem = lastSelectedItem.node; + } + }, + + /** + * Whether or not the given handler app is valid. + * + * @param aHandlerApp {nsIHandlerApp} the handler app in question + * + * @returns {boolean} whether or not it's valid + */ + isValidHandlerApp(aHandlerApp) { + if (!aHandlerApp) { + return false; + } + + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) { + return this._isValidHandlerExecutable(aHandlerApp.executable); + } + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) { + return aHandlerApp.uriTemplate; + } + + if (aHandlerApp instanceof Ci.nsIGIOMimeApp) { + return aHandlerApp.command; + } + + return false; + }, + + _isValidHandlerExecutable(aExecutable) { + let leafName; + if (AppConstants.platform == "win") { + leafName = `${AppConstants.MOZ_APP_NAME}.exe`; + } else if (AppConstants.platform == "macosx") { + leafName = AppConstants.MOZ_MACBUNDLE_NAME; + } else { + leafName = `${AppConstants.MOZ_APP_NAME}-bin`; + } + return ( + aExecutable && + aExecutable.exists() && + aExecutable.isExecutable() && + // XXXben - we need to compare this with the running instance executable + // just don't know how to do that via script... + // XXXmano TBD: can probably add this to nsIShellService + aExecutable.leafName != leafName + ); + }, + + /** + * Rebuild the actions menu for the selected entry. Gets called by + * the richlistitem constructor when an entry in the list gets selected. + */ + rebuildActionsMenu( + typeItem = this._list.selectedItem, + handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper + ) { + var menu = typeItem.querySelector(".actionsMenu"); + var menuPopup = menu.menupopup; + + // Clear out existing items. + while (menuPopup.hasChildNodes()) { + menuPopup.removeChild(menuPopup.lastChild); + } + + let internalMenuItem; + // Add the "Open in Firefox" option for optional internal handlers. + if ( + handlerInfo instanceof InternalHandlerInfoWrapper && + !handlerInfo.preventInternalViewing + ) { + internalMenuItem = document.createXULElement("menuitem"); + internalMenuItem.setAttribute( + "action", + Ci.nsIHandlerInfo.handleInternally + ); + document.l10n.setAttributes(internalMenuItem, "applications-open-inapp"); + internalMenuItem.setAttribute(APP_ICON_ATTR_NAME, "handleInternally"); + menuPopup.appendChild(internalMenuItem); + } + + var askMenuItem = document.createXULElement("menuitem"); + askMenuItem.setAttribute("action", Ci.nsIHandlerInfo.alwaysAsk); + document.l10n.setAttributes(askMenuItem, "applications-always-ask"); + askMenuItem.setAttribute(APP_ICON_ATTR_NAME, "ask"); + menuPopup.appendChild(askMenuItem); + + // Create a menu item for saving to disk. + // Note: this option isn't available to protocol types, since we don't know + // what it means to save a URL having a certain scheme to disk. + if (handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) { + var saveMenuItem = document.createXULElement("menuitem"); + saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk); + document.l10n.setAttributes(saveMenuItem, "applications-action-save"); + saveMenuItem.setAttribute(APP_ICON_ATTR_NAME, "save"); + menuPopup.appendChild(saveMenuItem); + } + + // Add a separator to distinguish these items from the helper app items + // that follow them. + let menuseparator = document.createXULElement("menuseparator"); + menuPopup.appendChild(menuseparator); + + // Create a menu item for the OS default application, if any. + if (handlerInfo.hasDefaultHandler) { + var defaultMenuItem = document.createXULElement("menuitem"); + defaultMenuItem.setAttribute( + "action", + Ci.nsIHandlerInfo.useSystemDefault + ); + // If an internal option is available, don't show the application + // name for the OS default to prevent two options from appearing + // that may both say "Firefox". + if (internalMenuItem) { + document.l10n.setAttributes( + defaultMenuItem, + "applications-use-os-default" + ); + defaultMenuItem.setAttribute("image", ICON_URL_APP); + } else { + document.l10n.setAttributes( + defaultMenuItem, + "applications-use-app-default", + { + "app-name": handlerInfo.defaultDescription, + } + ); + defaultMenuItem.setAttribute( + "image", + handlerInfo.iconURLForSystemDefault + ); + } + + menuPopup.appendChild(defaultMenuItem); + } + + // Create menu items for possible handlers. + let preferredApp = handlerInfo.preferredApplicationHandler; + var possibleAppMenuItems = []; + for (let possibleApp of handlerInfo.possibleApplicationHandlers.enumerate()) { + if (!this.isValidHandlerApp(possibleApp)) { + continue; + } + + let menuItem = document.createXULElement("menuitem"); + menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp); + let label; + if (possibleApp instanceof Ci.nsILocalHandlerApp) { + label = getFileDisplayName(possibleApp.executable); + } else { + label = possibleApp.name; + } + document.l10n.setAttributes(menuItem, "applications-use-app", { + "app-name": label, + }); + menuItem.setAttribute( + "image", + this._getIconURLForHandlerApp(possibleApp) + ); + + // Attach the handler app object to the menu item so we can use it + // to make changes to the datastore when the user selects the item. + menuItem.handlerApp = possibleApp; + + menuPopup.appendChild(menuItem); + possibleAppMenuItems.push(menuItem); + } + // Add gio handlers + if (gGIOService) { + var gioApps = gGIOService.getAppsForURIScheme(handlerInfo.type); + let possibleHandlers = handlerInfo.possibleApplicationHandlers; + for (let handler of gioApps.enumerate(Ci.nsIHandlerApp)) { + // OS handler share the same name, it's most likely the same app, skipping... + if (handler.name == handlerInfo.defaultDescription) { + continue; + } + // Check if the handler is already in possibleHandlers + let appAlreadyInHandlers = false; + for (let i = possibleHandlers.length - 1; i >= 0; --i) { + let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp); + // nsGIOMimeApp::Equals is able to compare with nsILocalHandlerApp + if (handler.equals(app)) { + appAlreadyInHandlers = true; + break; + } + } + if (!appAlreadyInHandlers) { + let menuItem = document.createXULElement("menuitem"); + menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp); + document.l10n.setAttributes(menuItem, "applications-use-app", { + "app-name": handler.name, + }); + menuItem.setAttribute( + "image", + this._getIconURLForHandlerApp(handler) + ); + + // Attach the handler app object to the menu item so we can use it + // to make changes to the datastore when the user selects the item. + menuItem.handlerApp = handler; + + menuPopup.appendChild(menuItem); + possibleAppMenuItems.push(menuItem); + } + } + } + + // Create a menu item for selecting a local application. + let canOpenWithOtherApp = true; + if (AppConstants.platform == "win") { + // On Windows, selecting an application to open another application + // would be meaningless so we special case executables. + let executableType = Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromExtension("exe"); + canOpenWithOtherApp = handlerInfo.type != executableType; + } + if (canOpenWithOtherApp) { + let menuItem = document.createXULElement("menuitem"); + menuItem.className = "choose-app-item"; + menuItem.addEventListener("command", function (e) { + gMainPane.chooseApp(e); + }); + document.l10n.setAttributes(menuItem, "applications-use-other"); + menuPopup.appendChild(menuItem); + } + + // Create a menu item for managing applications. + if (possibleAppMenuItems.length) { + let menuItem = document.createXULElement("menuseparator"); + menuPopup.appendChild(menuItem); + menuItem = document.createXULElement("menuitem"); + menuItem.className = "manage-app-item"; + menuItem.addEventListener("command", function (e) { + gMainPane.manageApp(e); + }); + document.l10n.setAttributes(menuItem, "applications-manage-app"); + menuPopup.appendChild(menuItem); + } + + // Select the item corresponding to the preferred action. If the always + // ask flag is set, it overrides the preferred action. Otherwise we pick + // the item identified by the preferred action (when the preferred action + // is to use a helper app, we have to pick the specific helper app item). + if (handlerInfo.alwaysAskBeforeHandling) { + menu.selectedItem = askMenuItem; + } else { + // The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify + // the actions the application can take with content of various types. + // But since we've stopped support for plugins, there's no value + // identifying the "use plugin" action, so we use this constant instead. + const kActionUsePlugin = 5; + + switch (handlerInfo.preferredAction) { + case Ci.nsIHandlerInfo.handleInternally: + if (internalMenuItem) { + menu.selectedItem = internalMenuItem; + } else { + console.error("No menu item defined to set!"); + } + break; + case Ci.nsIHandlerInfo.useSystemDefault: + // We might not have a default item if we're not aware of an + // OS-default handler for this type: + menu.selectedItem = defaultMenuItem || askMenuItem; + break; + case Ci.nsIHandlerInfo.useHelperApp: + if (preferredApp) { + let preferredItem = possibleAppMenuItems.find(v => + v.handlerApp.equals(preferredApp) + ); + if (preferredItem) { + menu.selectedItem = preferredItem; + } else { + // This shouldn't happen, but let's make sure we end up with a + // selected item: + let possible = possibleAppMenuItems + .map(v => v.handlerApp && v.handlerApp.name) + .join(", "); + console.error( + new Error( + `Preferred handler for ${handlerInfo.type} not in list of possible handlers!? (List: ${possible})` + ) + ); + menu.selectedItem = askMenuItem; + } + } + break; + case kActionUsePlugin: + // We no longer support plugins, select "ask" instead: + menu.selectedItem = askMenuItem; + break; + case Ci.nsIHandlerInfo.saveToDisk: + menu.selectedItem = saveMenuItem; + break; + } + } + }, + + // Sorting & Filtering + + _sortColumn: null, + + /** + * Sort the list when the user clicks on a column header. + */ + sort(event) { + if (event.button != 0) { + return; + } + var column = event.target; + + // If the user clicked on a new sort column, remove the direction indicator + // from the old column. + if (this._sortColumn && this._sortColumn != column) { + this._sortColumn.removeAttribute("sortDirection"); + } + + this._sortColumn = column; + + // Set (or switch) the sort direction indicator. + if (column.getAttribute("sortDirection") == "ascending") { + column.setAttribute("sortDirection", "descending"); + } else { + column.setAttribute("sortDirection", "ascending"); + } + + this._sortListView(); + }, + + async _sortListView() { + if (!this._sortColumn) { + return; + } + let comp = new Services.intl.Collator(undefined, { + usage: "sort", + }); + + await document.l10n.translateFragment(this._list); + let items = Array.from(this._list.children); + + let textForNode; + if (this._sortColumn.getAttribute("value") === "type") { + textForNode = n => n.querySelector(".typeDescription").textContent; + } else { + textForNode = n => n.querySelector(".actionsMenu").getAttribute("label"); + } + + let sortDir = this._sortColumn.getAttribute("sortDirection"); + let multiplier = sortDir == "descending" ? -1 : 1; + items.sort( + (a, b) => multiplier * comp.compare(textForNode(a), textForNode(b)) + ); + + // Re-append items in the correct order: + items.forEach(item => this._list.appendChild(item)); + }, + + _filterView(frag = this._list) { + const filterValue = this._filter.value.toLowerCase(); + for (let elem of frag.children) { + const typeDescription = + elem.querySelector(".typeDescription").textContent; + const actionDescription = elem + .querySelector(".actionDescription") + .getAttribute("value"); + elem.hidden = + !typeDescription.toLowerCase().includes(filterValue) && + !actionDescription.toLowerCase().includes(filterValue); + } + }, + + /** + * Filter the list when the user enters a filter term into the filter field. + */ + filter() { + this._rebuildView(); // FIXME: Should this be await since bug 1508156? + }, + + focusFilterBox() { + this._filter.focus(); + this._filter.select(); + }, + + // Changes + + // Whether or not we are currently storing the action selected by the user. + // We use this to suppress notification-triggered updates to the list when + // we make changes that may spawn such updates. + // XXXgijs: this was definitely necessary when we changed feed preferences + // from within _storeAction and its calltree. Now, it may still be + // necessary, to avoid calling _rebuildView. bug 1499350 has more details. + _storingAction: false, + + onSelectAction(aActionItem) { + this._storingAction = true; + + try { + this._storeAction(aActionItem); + } finally { + this._storingAction = false; + } + }, + + _storeAction(aActionItem) { + var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper; + + let action = parseInt(aActionItem.getAttribute("action")); + + // Set the preferred application handler. + // We leave the existing preferred app in the list when we set + // the preferred action to something other than useHelperApp so that + // legacy datastores that don't have the preferred app in the list + // of possible apps still include the preferred app in the list of apps + // the user can choose to handle the type. + if (action == Ci.nsIHandlerInfo.useHelperApp) { + handlerInfo.preferredApplicationHandler = aActionItem.handlerApp; + } + + // Set the "always ask" flag. + if (action == Ci.nsIHandlerInfo.alwaysAsk) { + handlerInfo.alwaysAskBeforeHandling = true; + } else { + handlerInfo.alwaysAskBeforeHandling = false; + } + + // Set the preferred action. + handlerInfo.preferredAction = action; + + handlerInfo.store(); + + // Update the action label and image to reflect the new preferred action. + this.selectedHandlerListItem.refreshAction(); + }, + + manageApp(aEvent) { + // Don't let the normal "on select action" handler get this event, + // as we handle it specially ourselves. + aEvent.stopPropagation(); + + var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper; + + let onComplete = () => { + // Rebuild the actions menu so that we revert to the previous selection, + // or "Always ask" if the previous default application has been removed + this.rebuildActionsMenu(); + + // update the richlistitem too. Will be visible when selecting another row + this.selectedHandlerListItem.refreshAction(); + }; + + gSubDialog.open( + "chrome://browser/content/preferences/dialogs/applicationManager.xhtml", + { features: "resizable=no", closingCallback: onComplete }, + handlerInfo + ); + }, + + async chooseApp(aEvent) { + // Don't let the normal "on select action" handler get this event, + // as we handle it specially ourselves. + aEvent.stopPropagation(); + + var handlerApp; + let chooseAppCallback = aHandlerApp => { + // Rebuild the actions menu whether the user picked an app or canceled. + // If they picked an app, we want to add the app to the menu and select it. + // If they canceled, we want to go back to their previous selection. + this.rebuildActionsMenu(); + + // If the user picked a new app from the menu, select it. + if (aHandlerApp) { + let typeItem = this._list.selectedItem; + let actionsMenu = typeItem.querySelector(".actionsMenu"); + let menuItems = actionsMenu.menupopup.childNodes; + for (let i = 0; i < menuItems.length; i++) { + let menuItem = menuItems[i]; + if (menuItem.handlerApp && menuItem.handlerApp.equals(aHandlerApp)) { + actionsMenu.selectedIndex = i; + this.onSelectAction(menuItem); + break; + } + } + } + }; + + if (AppConstants.platform == "win") { + var params = {}; + var handlerInfo = this.selectedHandlerListItem.handlerInfoWrapper; + + params.mimeInfo = handlerInfo.wrappedHandlerInfo; + params.title = await document.l10n.formatValue( + "applications-select-helper" + ); + if ("id" in handlerInfo.description) { + params.description = await document.l10n.formatValue( + handlerInfo.description.id, + handlerInfo.description.args + ); + } else { + params.description = handlerInfo.typeDescription.raw; + } + params.filename = null; + params.handlerApp = null; + + let onAppSelected = () => { + if (this.isValidHandlerApp(params.handlerApp)) { + handlerApp = params.handlerApp; + + // Add the app to the type's list of possible handlers. + handlerInfo.addPossibleApplicationHandler(handlerApp); + } + + chooseAppCallback(handlerApp); + }; + + gSubDialog.open( + "chrome://global/content/appPicker.xhtml", + { closingCallback: onAppSelected }, + params + ); + } else { + let winTitle = await document.l10n.formatValue( + "applications-select-helper" + ); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let fpCallback = aResult => { + if ( + aResult == Ci.nsIFilePicker.returnOK && + fp.file && + this._isValidHandlerExecutable(fp.file) + ) { + handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + handlerApp.name = getFileDisplayName(fp.file); + handlerApp.executable = fp.file; + + // Add the app to the type's list of possible handlers. + let handler = this.selectedHandlerListItem.handlerInfoWrapper; + handler.addPossibleApplicationHandler(handlerApp); + + chooseAppCallback(handlerApp); + } + }; + + // Prompt the user to pick an app. If they pick one, and it's a valid + // selection, then add it to the list of possible handlers. + fp.init(window, winTitle, Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + fp.open(fpCallback); + } + }, + + _getIconURLForHandlerApp(aHandlerApp) { + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) { + return this._getIconURLForFile(aHandlerApp.executable); + } + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) { + return this._getIconURLForWebApp(aHandlerApp.uriTemplate); + } + + // We know nothing about other kinds of handler apps. + return ""; + }, + + _getIconURLForFile(aFile) { + var fph = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + var urlSpec = fph.getURLSpecFromActualFile(aFile); + + return "moz-icon://" + urlSpec + "?size=16"; + }, + + _getIconURLForWebApp(aWebAppURITemplate) { + var uri = Services.io.newURI(aWebAppURITemplate); + + // Unfortunately we can't use the favicon service to get the favicon, + // because the service looks in the annotations table for a record with + // the exact URL we give it, and users won't have such records for URLs + // they don't visit, and users won't visit the web app's URL template, + // they'll only visit URLs derived from that template (i.e. with %s + // in the template replaced by the URL of the content being handled). + + if ( + /^https?$/.test(uri.scheme) && + Services.prefs.getBoolPref("browser.chrome.site_icons") + ) { + return uri.prePath + "/favicon.ico"; + } + + return ""; + }, + + // DOWNLOADS + + /* + * Preferences: + * + * browser.download.useDownloadDir - bool + * True - Save files directly to the folder configured via the + * browser.download.folderList preference. + * False - Always ask the user where to save a file and default to + * browser.download.lastDir when displaying a folder picker dialog. + * browser.download.always_ask_before_handling_new_types - bool + * Defines the default behavior for new file handlers. + * True - When downloading a file that doesn't match any existing + * handlers, ask the user whether to save or open the file. + * False - Save the file. The user can change the default action in + * the Applications section in the preferences UI. + * browser.download.dir - local file handle + * A local folder the user may have selected for downloaded files to be + * saved. Migration of other browser settings may also set this path. + * This folder is enabled when folderList equals 2. + * browser.download.lastDir - local file handle + * May contain the last folder path accessed when the user browsed + * via the file save-as dialog. (see contentAreaUtils.js) + * browser.download.folderList - int + * Indicates the location users wish to save downloaded files too. + * It is also used to display special file labels when the default + * download location is either the Desktop or the Downloads folder. + * Values: + * 0 - The desktop is the default download location. + * 1 - The system's downloads folder is the default download location. + * 2 - The default download location is elsewhere as specified in + * browser.download.dir. + * browser.download.downloadDir + * deprecated. + * browser.download.defaultFolder + * deprecated. + */ + + /** + * Disables the downloads folder field and Browse button if the default + * download directory pref is locked (e.g., by the DownloadDirectory or + * DefaultDownloadDirectory policies) + */ + readUseDownloadDir() { + document.getElementById("downloadFolder").disabled = + document.getElementById("chooseFolder").disabled = + document.getElementById("saveTo").disabled = + Preferences.get("browser.download.dir").locked || + Preferences.get("browser.download.folderList").locked; + // don't override the preference's value in UI + return undefined; + }, + + /** + * Displays a file picker in which the user can choose the location where + * downloads are automatically saved, updating preferences and UI in + * response to the choice, if one is made. + */ + chooseFolder() { + return this.chooseFolderTask().catch(console.error); + }, + async chooseFolderTask() { + let [title] = await document.l10n.formatValues([ + { id: "choose-download-folder-title" }, + ]); + let folderListPref = Preferences.get("browser.download.folderList"); + let currentDirPref = await this._indexToFolder(folderListPref.value); + let defDownloads = await this._indexToFolder(1); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + + fp.init(window, title, Ci.nsIFilePicker.modeGetFolder); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + // First try to open what's currently configured + if (currentDirPref && currentDirPref.exists()) { + fp.displayDirectory = currentDirPref; + } else if (defDownloads && defDownloads.exists()) { + // Try the system's download dir + fp.displayDirectory = defDownloads; + } else { + // Fall back to Desktop + fp.displayDirectory = await this._indexToFolder(0); + } + + let result = await new Promise(resolve => fp.open(resolve)); + if (result != Ci.nsIFilePicker.returnOK) { + return; + } + + let downloadDirPref = Preferences.get("browser.download.dir"); + downloadDirPref.value = fp.file; + folderListPref.value = await this._folderToIndex(fp.file); + // Note, the real prefs will not be updated yet, so dnld manager's + // userDownloadsDirectory may not return the right folder after + // this code executes. displayDownloadDirPref will be called on + // the assignment above to update the UI. + }, + + /** + * Initializes the download folder display settings based on the user's + * preferences. + */ + displayDownloadDirPref() { + this.displayDownloadDirPrefTask().catch(console.error); + + // don't override the preference's value in UI + return undefined; + }, + + async displayDownloadDirPrefTask() { + // We're async for localization reasons, and we can get called several + // times in the same turn of the event loop (!) because of how the + // preferences bindings work... but the speed of localization + // shouldn't impact what gets displayed to the user in the end - the + // last call should always win. + // To accomplish this, store a unique object when we enter this function, + // and if by the end of the function that stored object has been + // overwritten, don't update the UI but leave it to the last + // caller to this function to do. + let token = {}; + this._downloadDisplayToken = token; + + var downloadFolder = document.getElementById("downloadFolder"); + + let folderIndex = Preferences.get("browser.download.folderList").value; + // For legacy users using cloudstorage pref with folderIndex as 3 (See bug 1751093), + // compute folderIndex using the current directory pref + if (folderIndex == 3) { + let currentDirPref = Preferences.get("browser.download.dir"); + folderIndex = currentDirPref.value + ? await this._folderToIndex(currentDirPref.value) + : 1; + } + + // Display a 'pretty' label or the path in the UI. + let { folderDisplayName, file } = + await this._getSystemDownloadFolderDetails(folderIndex); + // Figure out an icon url: + let fph = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + let iconUrlSpec = fph.getURLSpecFromDir(file); + + // Ensure that the last entry to this function always wins + // (see comment at the start of this method): + if (this._downloadDisplayToken != token) { + return; + } + // note: downloadFolder.value is not read elsewhere in the code, its only purpose is to display to the user + downloadFolder.value = folderDisplayName; + downloadFolder.style.backgroundImage = + "url(moz-icon://" + iconUrlSpec + "?size=16)"; + }, + + async _getSystemDownloadFolderDetails(folderIndex) { + let downloadsDir = await this._getDownloadsFolder("Downloads"); + let desktopDir = await this._getDownloadsFolder("Desktop"); + let currentDirPref = Preferences.get("browser.download.dir"); + + let file; + let firefoxLocalizedName; + if (folderIndex == 2 && currentDirPref.value) { + file = currentDirPref.value; + if (file.equals(downloadsDir)) { + folderIndex = 1; + } else if (file.equals(desktopDir)) { + folderIndex = 0; + } + } + switch (folderIndex) { + case 2: // custom path, handled above. + break; + + case 1: { + // downloads + file = downloadsDir; + firefoxLocalizedName = await document.l10n.formatValues([ + { id: "downloads-folder-name" }, + ]); + break; + } + + case 0: + // fall through + default: { + file = desktopDir; + firefoxLocalizedName = await document.l10n.formatValues([ + { id: "desktop-folder-name" }, + ]); + } + } + if (firefoxLocalizedName) { + let folderDisplayName, leafName; + // Either/both of these can throw, so check for failures in both cases + // so we don't just break display of the download pref: + try { + folderDisplayName = file.displayName; + } catch (ex) { + /* ignored */ + } + try { + leafName = file.leafName; + } catch (ex) { + /* ignored */ + } + + // If we found a localized name that's different from the leaf name, + // use that: + if (folderDisplayName && folderDisplayName != leafName) { + return { file, folderDisplayName }; + } + + // Otherwise, check if we've got a localized name ourselves. + if (firefoxLocalizedName) { + // You can't move the system download or desktop dir on macOS, + // so if those are in use just display them. On other platforms + // only do so if the folder matches the localized name. + if ( + AppConstants.platform == "mac" || + leafName == firefoxLocalizedName + ) { + return { file, folderDisplayName: firefoxLocalizedName }; + } + } + } + // If we get here, attempts to use a "pretty" name failed. Just display + // the full path: + if (file) { + // Force the left-to-right direction when displaying a custom path. + return { file, folderDisplayName: `\u2066${file.path}\u2069` }; + } + // Don't even have a file - fall back to desktop directory for the + // use of the icon, and an empty label: + file = desktopDir; + return { file, folderDisplayName: "" }; + }, + + /** + * Returns the Downloads folder. If aFolder is "Desktop", then the Downloads + * folder returned is the desktop folder; otherwise, it is a folder whose name + * indicates that it is a download folder and whose path is as determined by + * the XPCOM directory service via the download manager's attribute + * defaultDownloadsDirectory. + * + * @throws if aFolder is not "Desktop" or "Downloads" + */ + async _getDownloadsFolder(aFolder) { + switch (aFolder) { + case "Desktop": + return Services.dirsvc.get("Desk", Ci.nsIFile); + case "Downloads": + let downloadsDir = await Downloads.getSystemDownloadsDirectory(); + return new FileUtils.File(downloadsDir); + } + throw new Error( + "ASSERTION FAILED: folder type should be 'Desktop' or 'Downloads'" + ); + }, + + /** + * Determines the type of the given folder. + * + * @param aFolder + * the folder whose type is to be determined + * @returns integer + * 0 if aFolder is the Desktop or is unspecified, + * 1 if aFolder is the Downloads folder, + * 2 otherwise + */ + async _folderToIndex(aFolder) { + if (!aFolder || aFolder.equals(await this._getDownloadsFolder("Desktop"))) { + return 0; + } else if (aFolder.equals(await this._getDownloadsFolder("Downloads"))) { + return 1; + } + return 2; + }, + + /** + * Converts an integer into the corresponding folder. + * + * @param aIndex + * an integer + * @returns the Desktop folder if aIndex == 0, + * the Downloads folder if aIndex == 1, + * the folder stored in browser.download.dir + */ + _indexToFolder(aIndex) { + switch (aIndex) { + case 0: + return this._getDownloadsFolder("Desktop"); + case 1: + return this._getDownloadsFolder("Downloads"); + } + var currentDirPref = Preferences.get("browser.download.dir"); + return currentDirPref.value; + }, +}; + +gMainPane.initialized = new Promise(res => { + gMainPane.setInitialized = res; +}); + +// Utilities + +function getFileDisplayName(file) { + if (AppConstants.platform == "win") { + if (file instanceof Ci.nsILocalFileWin) { + try { + return file.getVersionInfoField("FileDescription"); + } catch (e) {} + } + } + if (AppConstants.platform == "macosx") { + if (file instanceof Ci.nsILocalFileMac) { + try { + return file.bundleDisplayName; + } catch (e) {} + } + } + return file.leafName; +} + +function getLocalHandlerApp(aFile) { + var localHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandlerApp.name = getFileDisplayName(aFile); + localHandlerApp.executable = aFile; + + return localHandlerApp; +} + +// eslint-disable-next-line no-undef +let gHandlerListItemFragment = MozXULElement.parseXULToFragment(` + <richlistitem> + <hbox class="typeContainer" flex="1" align="center"> + <image class="typeIcon" width="16" height="16" + src="moz-icon://goat?size=16"/> + <label class="typeDescription" flex="1" crop="end"/> + </hbox> + <hbox class="actionContainer" flex="1" align="center"> + <image class="actionIcon" width="16" height="16"/> + <label class="actionDescription" flex="1" crop="end"/> + </hbox> + <hbox class="actionsMenuContainer" flex="1"> + <menulist class="actionsMenu" flex="1" crop="end" selectedIndex="1"> + <menupopup/> + </menulist> + </hbox> + </richlistitem> +`); + +/** + * This is associated to <richlistitem> elements in the handlers view. + */ +class HandlerListItem { + static forNode(node) { + return gNodeToObjectMap.get(node); + } + + constructor(handlerInfoWrapper) { + this.handlerInfoWrapper = handlerInfoWrapper; + } + + setOrRemoveAttributes(iterable) { + for (let [selector, name, value] of iterable) { + let node = selector ? this.node.querySelector(selector) : this.node; + if (value) { + node.setAttribute(name, value); + } else { + node.removeAttribute(name); + } + } + } + + createNode(list) { + list.appendChild(document.importNode(gHandlerListItemFragment, true)); + this.node = list.lastChild; + gNodeToObjectMap.set(this.node, this); + } + + setupNode() { + this.node + .querySelector(".actionsMenu") + .addEventListener("command", event => + gMainPane.onSelectAction(event.originalTarget) + ); + + let typeDescription = this.handlerInfoWrapper.typeDescription; + this.setOrRemoveAttributes([ + [null, "type", this.handlerInfoWrapper.type], + [".typeIcon", "src", this.handlerInfoWrapper.smallIcon], + ]); + localizeElement( + this.node.querySelector(".typeDescription"), + typeDescription + ); + this.showActionsMenu = false; + } + + refreshAction() { + let { actionIconClass } = this.handlerInfoWrapper; + this.setOrRemoveAttributes([ + [null, APP_ICON_ATTR_NAME, actionIconClass], + [ + ".actionIcon", + "src", + actionIconClass ? null : this.handlerInfoWrapper.actionIcon, + ], + ]); + const selectedItem = this.node.querySelector("[selected=true]"); + if (!selectedItem) { + console.error("No selected item for " + this.handlerInfoWrapper.type); + return; + } + const { id, args } = document.l10n.getAttributes(selectedItem); + localizeElement(this.node.querySelector(".actionDescription"), { + id: id + "-label", + args, + }); + localizeElement(this.node.querySelector(".actionsMenu"), { id, args }); + } + + set showActionsMenu(value) { + this.setOrRemoveAttributes([ + [".actionContainer", "hidden", value], + [".actionsMenuContainer", "hidden", !value], + ]); + } +} + +/** + * This API facilitates dual-model of some localization APIs which + * may operate on raw strings of l10n id/args pairs. + * + * The l10n can be: + * + * {raw: string} - raw strings to be used as text value of the element + * {id: string} - l10n-id + * {id: string, args: object} - l10n-id + l10n-args + */ +function localizeElement(node, l10n) { + if (l10n.hasOwnProperty("raw")) { + node.removeAttribute("data-l10n-id"); + node.textContent = l10n.raw; + } else { + document.l10n.setAttributes(node, l10n.id, l10n.args); + } +} + +/** + * This object wraps nsIHandlerInfo with some additional functionality + * the Applications prefpane needs to display and allow modification of + * the list of handled types. + * + * We create an instance of this wrapper for each entry we might display + * in the prefpane, and we compose the instances from various sources, + * including the handler service. + * + * We don't implement all the original nsIHandlerInfo functionality, + * just the stuff that the prefpane needs. + */ +class HandlerInfoWrapper { + constructor(type, handlerInfo) { + this.type = type; + this.wrappedHandlerInfo = handlerInfo; + this.disambiguateDescription = false; + } + + get description() { + if (this.wrappedHandlerInfo.description) { + return { raw: this.wrappedHandlerInfo.description }; + } + + if (this.primaryExtension) { + var extension = this.primaryExtension.toUpperCase(); + return { id: "applications-file-ending", args: { extension } }; + } + + return { raw: this.type }; + } + + /** + * Describe, in a human-readable fashion, the type represented by the given + * handler info object. Normally this is just the description, but if more + * than one object presents the same description, "disambiguateDescription" + * is set and we annotate the duplicate descriptions with the type itself + * to help users distinguish between those types. + */ + get typeDescription() { + if (this.disambiguateDescription) { + const description = this.description; + if (description.id) { + // Pass through the arguments: + let { args = {} } = description; + args.type = this.type; + return { + id: description.id + "-with-type", + args, + }; + } + + return { + id: "applications-type-description-with-type", + args: { + "type-description": description.raw, + type: this.type, + }, + }; + } + + return this.description; + } + + get actionIconClass() { + if (this.alwaysAskBeforeHandling) { + return "ask"; + } + + switch (this.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + return "save"; + + case Ci.nsIHandlerInfo.handleInternally: + if (this instanceof InternalHandlerInfoWrapper) { + return "handleInternally"; + } + break; + } + + return ""; + } + + get actionIcon() { + switch (this.preferredAction) { + case Ci.nsIHandlerInfo.useSystemDefault: + return this.iconURLForSystemDefault; + + case Ci.nsIHandlerInfo.useHelperApp: + let preferredApp = this.preferredApplicationHandler; + if (gMainPane.isValidHandlerApp(preferredApp)) { + return gMainPane._getIconURLForHandlerApp(preferredApp); + } + + // This should never happen, but if preferredAction is set to some weird + // value, then fall back to the generic application icon. + // Explicit fall-through + default: + return ICON_URL_APP; + } + } + + get iconURLForSystemDefault() { + // Handler info objects for MIME types on some OSes implement a property bag + // interface from which we can get an icon for the default app, so if we're + // dealing with a MIME type on one of those OSes, then try to get the icon. + if ( + this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + this.wrappedHandlerInfo instanceof Ci.nsIPropertyBag + ) { + try { + let url = this.wrappedHandlerInfo.getProperty( + "defaultApplicationIconURL" + ); + if (url) { + return url + "?size=16"; + } + } catch (ex) {} + } + + // If this isn't a MIME type object on an OS that supports retrieving + // the icon, or if we couldn't retrieve the icon for some other reason, + // then use a generic icon. + return ICON_URL_APP; + } + + get preferredApplicationHandler() { + return this.wrappedHandlerInfo.preferredApplicationHandler; + } + + set preferredApplicationHandler(aNewValue) { + this.wrappedHandlerInfo.preferredApplicationHandler = aNewValue; + + // Make sure the preferred handler is in the set of possible handlers. + if (aNewValue) { + this.addPossibleApplicationHandler(aNewValue); + } + } + + get possibleApplicationHandlers() { + return this.wrappedHandlerInfo.possibleApplicationHandlers; + } + + addPossibleApplicationHandler(aNewHandler) { + for (let app of this.possibleApplicationHandlers.enumerate()) { + if (app.equals(aNewHandler)) { + return; + } + } + this.possibleApplicationHandlers.appendElement(aNewHandler); + } + + removePossibleApplicationHandler(aHandler) { + var defaultApp = this.preferredApplicationHandler; + if (defaultApp && aHandler.equals(defaultApp)) { + // If the app we remove was the default app, we must make sure + // it won't be used anymore + this.alwaysAskBeforeHandling = true; + this.preferredApplicationHandler = null; + } + + var handlers = this.possibleApplicationHandlers; + for (var i = 0; i < handlers.length; ++i) { + var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if (handler.equals(aHandler)) { + handlers.removeElementAt(i); + break; + } + } + } + + get hasDefaultHandler() { + return this.wrappedHandlerInfo.hasDefaultHandler; + } + + get defaultDescription() { + return this.wrappedHandlerInfo.defaultDescription; + } + + // What to do with content of this type. + get preferredAction() { + // If the action is to use a helper app, but we don't have a preferred + // handler app, then switch to using the system default, if any; otherwise + // fall back to saving to disk, which is the default action in nsMIMEInfo. + // Note: "save to disk" is an invalid value for protocol info objects, + // but the alwaysAskBeforeHandling getter will detect that situation + // and always return true in that case to override this invalid value. + if ( + this.wrappedHandlerInfo.preferredAction == + Ci.nsIHandlerInfo.useHelperApp && + !gMainPane.isValidHandlerApp(this.preferredApplicationHandler) + ) { + if (this.wrappedHandlerInfo.hasDefaultHandler) { + return Ci.nsIHandlerInfo.useSystemDefault; + } + return Ci.nsIHandlerInfo.saveToDisk; + } + + return this.wrappedHandlerInfo.preferredAction; + } + + set preferredAction(aNewValue) { + this.wrappedHandlerInfo.preferredAction = aNewValue; + } + + get alwaysAskBeforeHandling() { + // If this is a protocol type and the preferred action is "save to disk", + // which is invalid for such types, then return true here to override that + // action. This could happen when the preferred action is to use a helper + // app, but the preferredApplicationHandler is invalid, and there isn't + // a default handler, so the preferredAction getter returns save to disk + // instead. + if ( + !(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) && + this.preferredAction == Ci.nsIHandlerInfo.saveToDisk + ) { + return true; + } + + return this.wrappedHandlerInfo.alwaysAskBeforeHandling; + } + + set alwaysAskBeforeHandling(aNewValue) { + this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue; + } + + // The primary file extension associated with this type, if any. + get primaryExtension() { + try { + if ( + this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + this.wrappedHandlerInfo.primaryExtension + ) { + return this.wrappedHandlerInfo.primaryExtension; + } + } catch (ex) {} + + return null; + } + + store() { + gHandlerService.store(this.wrappedHandlerInfo); + } + + get smallIcon() { + return this._getIcon(16); + } + + _getIcon(aSize) { + if (this.primaryExtension) { + return "moz-icon://goat." + this.primaryExtension + "?size=" + aSize; + } + + if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) { + return "moz-icon://goat?size=" + aSize + "&contentType=" + this.type; + } + + // FIXME: consider returning some generic icon when we can't get a URL for + // one (for example in the case of protocol schemes). Filed as bug 395141. + return null; + } +} + +/** + * InternalHandlerInfoWrapper provides a basic mechanism to create an internal + * mime type handler that can be enabled/disabled in the applications preference + * menu. + */ +class InternalHandlerInfoWrapper extends HandlerInfoWrapper { + constructor(mimeType, extension) { + let type = gMIMEService.getFromTypeAndExtension(mimeType, extension); + super(mimeType || type.type, type); + } + + // Override store so we so we can notify any code listening for registration + // or unregistration of this handler. + store() { + super.store(); + } + + get preventInternalViewing() { + return false; + } + + get enabled() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } +} + +class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper { + constructor() { + super(TYPE_PDF, null); + } + + get preventInternalViewing() { + return Services.prefs.getBoolPref(PREF_PDFJS_DISABLED); + } + + // PDF is always shown in the list, but the 'show internally' option is + // hidden when the internal PDF viewer is disabled. + get enabled() { + return true; + } +} + +class ViewableInternallyHandlerInfoWrapper extends InternalHandlerInfoWrapper { + get enabled() { + return DownloadIntegration.shouldViewDownloadInternally(this.type); + } +} + +const AppearanceChooser = { + // NOTE: This order must match the values of the + // layout.css.prefers-color-scheme.content-override + // preference. + choices: ["dark", "light", "auto"], + chooser: null, + radios: null, + warning: null, + + init() { + this.chooser = document.getElementById("web-appearance-chooser"); + this.radios = [...this.chooser.querySelectorAll("input")]; + for (let radio of this.radios) { + radio.addEventListener("change", e => { + let index = this.choices.indexOf(e.target.value); + // The pref change callback will update state if needed. + if (index >= 0) { + Services.prefs.setIntPref(PREF_CONTENT_APPEARANCE, index); + } else { + // Shouldn't happen but let's do something sane... + Services.prefs.clearUserPref(PREF_CONTENT_APPEARANCE); + } + }); + } + + // Forward the click to the "colors" button. + document + .getElementById("web-appearance-manage-colors-link") + .addEventListener("click", function (e) { + document.getElementById("colors").click(); + e.preventDefault(); + }); + + document + .getElementById("web-appearance-manage-themes-link") + .addEventListener("click", function (e) { + window.browsingContext.topChromeWindow.BrowserOpenAddonsMgr( + "addons://list/theme" + ); + e.preventDefault(); + }); + + this.warning = document.getElementById("web-appearance-override-warning"); + + FORCED_COLORS_QUERY.addEventListener("change", this); + Services.prefs.addObserver(PREF_USE_SYSTEM_COLORS, this); + Services.obs.addObserver(this, "look-and-feel-changed"); + this._update(); + }, + + _update() { + this._updateWarning(); + this._updateOptions(); + }, + + handleEvent(e) { + this._update(); + }, + + observe(subject, topic, data) { + this._update(); + }, + + destroy() { + Services.prefs.removeObserver(PREF_USE_SYSTEM_COLORS, this); + Services.obs.removeObserver(this, "look-and-feel-changed"); + FORCED_COLORS_QUERY.removeEventListener("change", this); + }, + + _isValueDark(value) { + switch (value) { + case "light": + return false; + case "dark": + return true; + case "auto": + return Services.appinfo.contentThemeDerivedColorSchemeIsDark; + } + throw new Error("Unknown value"); + }, + + _updateOptions() { + let index = Services.prefs.getIntPref(PREF_CONTENT_APPEARANCE); + if (index < 0 || index >= this.choices.length) { + index = Services.prefs + .getDefaultBranch(null) + .getIntPref(PREF_CONTENT_APPEARANCE); + } + let value = this.choices[index]; + for (let radio of this.radios) { + let checked = radio.value == value; + let isDark = this._isValueDark(radio.value); + + radio.checked = checked; + radio.closest("label").classList.toggle("dark", isDark); + } + }, + + _updateWarning() { + let forcingColorsAndNoColorSchemeSupport = + FORCED_COLORS_QUERY.matches && + (AppConstants.platform == "win" || + !Services.prefs.getBoolPref(PREF_USE_SYSTEM_COLORS)); + this.warning.hidden = !forcingColorsAndNoColorSchemeSupport; + }, +}; |