gecko-dev/browser/components/preferences/main.js

3685 строки
121 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* import-globals-from extensionControlled.js */
/* import-globals-from preferences.js */
/* import-globals-from ../../../toolkit/mozapps/preferences/fontbuilder.js */
/* import-globals-from ../../base/content/aboutDialog-appUpdater.js */
/* global MozXULElement */
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
var { Downloads } = ChromeUtils.import("resource://gre/modules/Downloads.jsm");
var { FileUtils } = ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
var { TransientPrefs } = ChromeUtils.import(
"resource:///modules/TransientPrefs.jsm"
);
var { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
var { L10nRegistry } = ChromeUtils.import(
"resource://gre/modules/L10nRegistry.jsm"
);
var { HomePage } = ChromeUtils.import("resource:///modules/HomePage.jsm");
ChromeUtils.defineModuleGetter(
this,
"CloudStorage",
"resource://gre/modules/CloudStorage.jsm"
);
var { Integration } = ChromeUtils.import(
"resource://gre/modules/Integration.jsm"
);
/* global DownloadIntegration */
Integration.downloads.defineModuleGetter(
this,
"DownloadIntegration",
"resource://gre/modules/DownloadIntegration.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"SelectionChangedMenulist",
"resource:///modules/SelectionChangedMenulist.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"UpdateUtils",
"resource://gre/modules/UpdateUtils.jsm"
);
XPCOMUtils.defineLazyServiceGetters(this, {
gHandlerService: [
"@mozilla.org/uriloader/handler-service;1",
"nsIHandlerService",
],
gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
});
// Constants & Enumeration Values
const TYPE_PDF = "application/pdf";
const PREF_PDFJS_DISABLED = "pdfjs.disabled";
const PREF_DISABLED_PLUGIN_TYPES = "plugin.disable_full_page_plugin_for_types";
// Pref for when containers is being controlled
const PREF_CONTAINERS_EXTENSION = "privacy.userContext.extension";
// Preferences that affect which entries to show in the list.
const PREF_SHOW_PLUGINS_IN_LIST = "browser.download.show_plugins_in_list";
const PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS =
"browser.download.hide_plugins_without_extensions";
// Strings to identify ExtensionSettingsStore overrides
const CONTAINERS_KEY = "privacy.containers";
const AUTO_UPDATE_CHANGED_TOPIC = "auto-update-config-change";
// The nsHandlerInfoAction enumeration values in nsIHandlerInfo identify
// the actions the application can take with content of various types.
// But since nsIHandlerInfo doesn't support plugins, there's no value
// identifying the "use plugin" action, so we use this constant instead.
const kActionUsePlugin = 5;
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", "handleInternally" or "plugin". 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";
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
if (AppConstants.MOZ_DEV_EDITION) {
ChromeUtils.defineModuleGetter(
this,
"fxAccounts",
"resource://gre/modules/FxAccounts.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FxAccounts",
"resource://gre/modules/FxAccounts.jsm"
);
}
Preferences.addAll([
// Startup
{ id: "browser.startup.page", type: "int" },
{ id: "browser.privatebrowsing.autostart", type: "bool" },
{ id: "browser.sessionstore.warnOnQuit", type: "bool" },
// Downloads
{ id: "browser.download.useDownloadDir", 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.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.tabs.warnOnOpen", type: "bool" },
{ id: "browser.ctrlTab.recentlyUsedOrder", 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: "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",
},
]);
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.MOZ_MAINTENANCE_SERVICE) {
Preferences.addAll([{ id: "app.update.service.enabled", type: "bool" }]);
}
}
// 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,
])
);
function generateBundles(resourceIds) {
return L10nRegistry.generateBundles(locales, resourceIds);
}
return new Localization(
["browser/preferences/preferences.ftl", "branding/brand.ftl"],
false,
{ generateBundles }
);
}
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();
let performanceSettingsLink = document.getElementById(
"performanceSettingsLearnMore"
);
let performanceSettingsUrl =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"performance";
performanceSettingsLink.setAttribute("href", performanceSettingsUrl);
this.updateDefaultPerformanceSettingsPref();
let defaultPerformancePref = Preferences.get(
"browser.preferences.defaultPerformanceSettings.enabled"
);
defaultPerformancePref.on("change", () => {
this.updatePerformanceSettingsBox({ duringChangeEvent: true });
});
this.updatePerformanceSettingsBox({ duringChangeEvent: false });
this.displayUseSystemLocale();
let connectionSettingsLink = document.getElementById(
"connectionSettingsLearnMore"
);
let connectionSettingsUrl =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"prefs-connection-settings";
connectionSettingsLink.setAttribute("href", connectionSettingsUrl);
this.updateProxySettingsUI();
initializeProxyUI(gMainPane);
if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
gMainPane.initBrowserLocale();
}
// We call `initDefaultZoomValues` to set and unhide the
// default zoom preferences menu, and to establish a
// listener for future menu changes.
gMainPane.initDefaultZoomValues();
let cfrLearnMoreUrl =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"extensionrecommendations";
for (const id of ["cfrLearnMore", "cfrFeaturesLearnMore"]) {
let link = document.getElementById(id);
link.setAttribute("href", cfrLearnMoreUrl);
}
if (
Services.prefs.getBoolPref(
"media.videocontrols.picture-in-picture.enabled"
)
) {
document.getElementById("pictureInPictureBox").hidden = false;
let pipLearnMoreUrl =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"picture-in-picture";
let link = document.getElementById("pictureInPictureLearnMore");
link.setAttribute("href", pipLearnMoreUrl);
}
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 "closing multiple tabs" and "opening multiple tabs might slow down
// &brandShortName;" warnings provide options for not showing these
// warnings again. When the user disabled them, we provide checkboxes to
// re-enable the warnings.
if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnClose")) {
document.getElementById("warnCloseMultiple").hidden = true;
}
if (!TransientPrefs.prefShouldBeVisible("browser.tabs.warnOnOpen")) {
document.getElementById("warnOpenMany").hidden = true;
}
setEventListener("ctrlTabRecentlyUsedOrder", "command", function() {
Services.prefs.clearUserPref("browser.ctrlTab.migrated");
});
setEventListener("manageBrowserLanguagesButton", "command", function() {
gMainPane.showBrowserLanguages({ 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();
});
}
// 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
);
setEventListener(
"translateButton",
"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
);
// Initializes the fonts dropdowns displayed in this pane.
this._rebuildFonts();
this.updateOnScreenKeyboardVisibility();
// Show translation preferences if we may:
const prefName = "browser.translation.ui.show";
if (Services.prefs.getBoolPref(prefName)) {
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");
}
}
let drmInfoURL =
Services.urlFormatter.formatURLPref("app.support.baseURL") +
"drm-content";
document
.getElementById("playDRMContentLink")
.setAttribute("href", drmInfoURL);
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 distroId = Services.prefs.getCharPref("distribution.id", "");
if (distroId) {
let distroString = distroId;
let distroVersion = Services.prefs.getCharPref(
"distribution.version",
""
);
if (distroVersion) {
distroString += " - " + distroVersion;
}
let distroIdField = document.getElementById("distributionId");
distroIdField.value = distroString;
distroIdField.hidden = false;
let distroAbout = Services.prefs.getStringPref("distribution.about", "");
if (distroAbout) {
let distroField = document.getElementById("distribution");
distroField.value = distroAbout;
distroField.hidden = false;
}
}
if (AppConstants.MOZ_UPDATER) {
// XXX Workaround bug 1523453 -- changing selectIndex of a <deck> before
// frame construction could confuse nsDeckFrame::RemoveFrame().
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
gAppUpdater = new appUpdater();
});
});
setEventListener("showUpdateHistory", "command", gMainPane.showUpdates);
let updateDisabled =
Services.policies && !Services.policies.isAllowed("appUpdate");
if (updateDisabled || UpdateUtils.appUpdateAutoSettingIsLocked()) {
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.updateReadPrefs();
setEventListener(
"updateRadioGroup",
"command",
gMainPane.updateWritePrefs
);
}
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("updateSettingCrossUserWarning").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.prefs.addObserver(PREF_SHOW_PLUGINS_IN_LIST, this);
Services.prefs.addObserver(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS, this);
Services.obs.addObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
setEventListener("filter", "command", gMainPane.filter);
setEventListener("typeColumn", "click", gMainPane.sort);
setEventListener("actionColumn", "click", gMainPane.sort);
setEventListener("chooseFolder", "command", gMainPane.chooseFolder);
setEventListener("saveWhere", "command", gMainPane.handleSaveToCommand);
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"),
]);
// 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("saveWhere"),
() => 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 }
);
});
},
// 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);
const link = document.getElementById("browserContainersLearnMore");
link.href =
Services.urlFormatter.formatURLPref("app.support.baseURL") + "containers";
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;
}
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");
let warnOnQuitCheckbox = document.getElementById(
"browserRestoreSessionQuitWarning"
);
if (pbAutoStartPref.value || startupPref.locked) {
checkbox.setAttribute("disabled", "true");
warnOnQuitCheckbox.setAttribute("disabled", "true");
} else {
checkbox.removeAttribute("disabled");
}
newValue = pbAutoStartPref.value
? false
: startupPref.value === this.STARTUP_PREF_RESTORE_SESSION;
if (checkbox.checked !== newValue) {
checkbox.checked = newValue;
let warnOnQuitPref = Preferences.get("browser.sessionstore.warnOnQuit");
if (newValue && !warnOnQuitPref.locked && !pbAutoStartPref.value) {
warnOnQuitCheckbox.removeAttribute("disabled");
} else {
warnOnQuitCheckbox.setAttribute("disabled", "true");
}
}
},
/**
* 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;
},
initBrowserLocale() {
// Enable telemetry.
Services.telemetry.setEventRecordingEnabled(
"intl.ui.browserLanguage",
true
);
// This will register the "command" listener.
let menulist = document.getElementById("defaultBrowserLanguage");
new SelectionChangedMenulist(menulist, event => {
gMainPane.onBrowserLanguageChange(event);
});
gMainPane.setBrowserLocales(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.
*/
async setBrowserLocales(selected) {
let available = await getAvailableLocales();
let localeNames = Services.intl.getLocaleDisplayNames(undefined, available);
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 = "defaultBrowserLanguageSearch";
menuitem.setAttribute(
"label",
await document.l10n.formatValue("browser-languages-search")
);
menuitem.setAttribute("value", "search");
fragment.appendChild(menuitem);
}
let menulist = document.getElementById("defaultBrowserLanguage");
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.setAttribute("flex", "1");
messageContainer.setAttribute("align", "center");
let description = document.createXULElement("description");
description.classList.add("message-bar-description");
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.selectedLocales = 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. */
onBrowserLanguageChange(event) {
let locale = event.target.value;
if (locale == "search") {
gMainPane.showBrowserLanguages({ search: true });
return;
} else if (locale == Services.locale.appLocaleAsBCP47) {
this.hideConfirmLanguageChangeMessageBar();
return;
}
// Note the change in telemetry.
gMainPane.recordBrowserLanguagesTelemetry("reorder");
let locales = Array.from(
new Set([locale, ...Services.locale.requestedLocales]).values()
);
this.showConfirmLanguageChangeMessageBar(locales);
},
/**
* 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;
let warnOnQuitCheckbox = document.getElementById(
"browserRestoreSessionQuitWarning"
);
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;
let warnOnQuitPref = Preferences.get("browser.sessionstore.warnOnQuit");
if (!warnOnQuitPref.locked) {
warnOnQuitCheckbox.removeAttribute("disabled");
}
} else {
newValue = this.STARTUP_PREF_HOMEPAGE;
warnOnQuitCheckbox.setAttribute("disabled", "true");
}
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.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");
if (!shellSvc) {
defaultBrowserBox.hidden = true;
return;
}
let setDefaultPane = document.getElementById("setDefaultPane");
let isDefault = shellSvc.isDefaultBrowser(false, true);
setDefaultPane.selectedIndex = isDefault ? 1 : 0;
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) {
Cu.reportError(ex);
return;
}
let selectedIndex = shellSvc.isDefaultBrowser(false, true) ? 1 : 0;
document.getElementById("setDefaultPane").selectedIndex = selectedIndex;
}
},
/**
* 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
);
},
showBrowserLanguages({ 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 = { selected: gMainPane.selectedLocales, search, telemetryId };
gSubDialog.open(
"chrome://browser/content/preferences/dialogs/browserLanguages.xhtml",
{ closingCallback: this.browserLanguagesClosed },
opts
);
},
/* Show or hide the confirm change message bar based on the updated ordering. */
browserLanguagesClosed() {
let { accepted, selected } = this.gBrowserLanguagesDialog;
let active = Services.locale.appLocalesAsBCP47;
this.gBrowserLanguagesDialog.recordTelemetry(
accepted ? "accept" : "cancel"
);
// Prepare for changing the locales if they are different than the current locales.
if (selected && selected.join(",") != active.join(",")) {
gMainPane.showConfirmLanguageChangeMessageBar(selected);
gMainPane.setBrowserLocales(selected[0]);
return;
}
// They matched, so we can reset the UI.
gMainPane.setBrowserLocales(Services.locale.appLocaleAsBCP47);
gMainPane.hideConfirmLanguageChangeMessageBar();
},
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,
]);
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.
*/
showTranslationExceptions() {
gSubDialog.open(
"chrome://browser/content/preferences/dialogs/translation.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(Cu.reportError);
},
/**
* 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.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;
}
},
/**
* Selects the correct item in the update radio group
*/
async updateReadPrefs() {
if (
AppConstants.MOZ_UPDATER &&
(!Services.policies || Services.policies.isAllowed("appUpdate"))
) {
let radiogroup = document.getElementById("updateRadioGroup");
radiogroup.disabled = true;
try {
let enabled = await UpdateUtils.getAppUpdateAutoEnabled();
radiogroup.value = enabled;
radiogroup.disabled = false;
} catch (error) {
Cu.reportError(error);
}
}
},
/**
* Writes the value of the update radio group to the disk
*/
async updateWritePrefs() {
if (
AppConstants.MOZ_UPDATER &&
(!Services.policies || Services.policies.isAllowed("appUpdate"))
) {
let radiogroup = document.getElementById("updateRadioGroup");
let updateAutoValue = radiogroup.value == "true";
radiogroup.disabled = true;
try {
await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue);
radiogroup.disabled = false;
} catch (error) {
Cu.reportError(error);
await this.updateReadPrefs();
await this.reportUpdatePrefWriteError(error);
return;
}
// 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();
}
}
},
async reportUpdatePrefWriteError(error) {
let [title, message] = await document.l10n.formatValues([
{ id: "update-setting-write-failure-title" },
{
id: "update-setting-write-failure-message",
args: { path: error.path },
},
]);
// 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.activeUpdate) {
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
);
aus.stopDownload();
um.cleanupActiveUpdate();
}
},
/**
* Displays the history of installed updates.
*/
showUpdates() {
gSubDialog.open("chrome://mozapps/content/update/history.xhtml");
},
destroy() {
window.removeEventListener("unload", this);
Services.prefs.removeObserver(PREF_SHOW_PLUGINS_IN_LIST, this);
Services.prefs.removeObserver(PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS, this);
Services.prefs.removeObserver(PREF_CONTAINERS_EXTENSION, this);
Services.obs.removeObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
},
// 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) {
// These two prefs alter the list of visible types, so we have to rebuild
// that list when they change.
if (
aData == PREF_SHOW_PLUGINS_IN_LIST ||
aData == PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS
) {
await this._rebuildVisibleTypes();
await this._rebuildView();
await this._sortListView();
} else {
await this._rebuildView();
}
}
} else if (aTopic == AUTO_UPDATE_CHANGED_TOPIC) {
if (aData != "true" && aData != "false") {
throw new Error("Invalid preference value for app.update.auto");
}
document.getElementById("updateRadioGroup").value = aData;
}
},
// EventListener
handleEvent(aEvent) {
if (aEvent.type == "unload") {
this.destroy();
if (AppConstants.MOZ_UPDATER) {
onUnload();
}
}
},
// Composed Model Construction
_loadData() {
this._loadInternalHandlers();
this._loadPluginHandlers();
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(ext.trim())
);
}
}
for (let internalHandler of internalHandlers) {
if (internalHandler.enabled) {
this._handledTypes[internalHandler.type] = internalHandler;
}
}
},
/**
* Load the set of handlers defined by plugins.
*
* Note: if there's more than one plugin for a given MIME type, we assume
* the last one is the one that the application will use. That may not be
* correct, but it's how we've been doing it for years.
*
* Perhaps we should instead query navigator.mimeTypes for the set of types
* supported by the application and then get the plugin from each MIME type's
* enabledPlugin property. But if there's a plugin for a type, we need
* to know about it even if it isn't enabled, since we're going to give
* the user an option to enable it.
*
* Also note that enabledPlugin does not get updated when
* plugin.disable_full_page_plugin_for_types changes, so even if we could use
* enabledPlugin to get the plugin that would be used, we'd still need to
* check the pref ourselves to find out if it's enabled.
*/
_loadPluginHandlers() {
"use strict";
let mimeTypes = navigator.mimeTypes;
for (let mimeType of mimeTypes) {
let handlerInfoWrapper;
if (mimeType.type in this._handledTypes) {
handlerInfoWrapper = this._handledTypes[mimeType.type];
} else {
let wrappedHandlerInfo = gMIMEService.getFromTypeAndExtension(
mimeType.type,
null
);
handlerInfoWrapper = new HandlerInfoWrapper(
mimeType.type,
wrappedHandlerInfo
);
handlerInfoWrapper.handledOnlyByPlugin = true;
this._handledTypes[mimeType.type] = handlerInfoWrapper;
}
handlerInfoWrapper.pluginName = mimeType.enabledPlugin.name;
}
},
/**
* 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 {
handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo);
this._handledTypes[type] = handlerInfoWrapper;
}
handlerInfoWrapper.handledOnlyByPlugin = false;
}
},
// 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();
// Get the preferences that help determine what types to show.
var showPlugins = Services.prefs.getBoolPref(PREF_SHOW_PLUGINS_IN_LIST);
var hidePluginsWithoutExtensions = Services.prefs.getBoolPref(
PREF_HIDE_PLUGINS_WITHOUT_EXTENSIONS
);
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];
// Hide plugins without associated extensions if so prefed so we don't
// show a whole bunch of obscure types handled by plugins on Mac.
// Note: though protocol types don't have extensions, we still show them;
// the pref is only meant to be applied to MIME types, since plugins are
// only associated with MIME types.
// FIXME: should we also check the "suffixes" property of the plugin?
// Filed as bug 395135.
if (
hidePluginsWithoutExtensions &&
handlerInfo.handledOnlyByPlugin &&
handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
!handlerInfo.primaryExtension
) {
continue;
}
// Hide types handled only by plugins if so prefed.
if (handlerInfo.handledOnlyByPlugin && !showPlugins) {
continue;
}
// 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) {
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 (Cc["@mozilla.org/gio-service;1"]) {
let gIOSvc = Cc["@mozilla.org/gio-service;1"].getService(
Ci.nsIGIOService
);
var gioApps = gIOSvc.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 the plugin.
if (handlerInfo.pluginName) {
var pluginMenuItem = document.createXULElement("menuitem");
pluginMenuItem.setAttribute("action", kActionUsePlugin);
document.l10n.setAttributes(
pluginMenuItem,
"applications-use-plugin-in",
{
"plugin-name": handlerInfo.pluginName,
}
);
pluginMenuItem.setAttribute(APP_ICON_ATTR_NAME, "plugin");
menuPopup.appendChild(pluginMenuItem);
}
// 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 {
switch (handlerInfo.preferredAction) {
case Ci.nsIHandlerInfo.handleInternally:
if (internalMenuItem) {
menu.selectedItem = internalMenuItem;
} else {
Cu.reportError("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(", ");
Cu.reportError(
new Error(
`Preferred handler for ${handlerInfo.type} not in list of possible handlers!? (List: ${possible})`
)
);
menu.selectedItem = askMenuItem;
}
}
break;
case kActionUsePlugin:
// The plugin may have been removed, if so, select 'always ask':
menu.selectedItem = pluginMenuItem || 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) {
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, either to avoid calling _rebuildView or to avoid the plugin-
// related prefs change code. 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 plugin state if we're enabling or disabling a plugin.
if (action == kActionUsePlugin) {
handlerInfo.enablePluginType();
} else if (handlerInfo.pluginName && !handlerInfo.isDisabledPluginType) {
handlerInfo.disablePluginType();
}
// 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();
// Make sure the handler info object is flagged to indicate that there is
// now some user configuration for the type.
handlerInfo.handledOnlyByPlugin = false;
// 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.getURLSpecFromFile(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.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.
* 3 - The default download location is elsewhere as specified by
* cloud storage API getDownloadFolder
* browser.download.downloadDir
* deprecated.
* browser.download.defaultFolder
* deprecated.
*/
/**
* Enables/disables the folder field and Browse button based on whether a
* default download directory is being used.
*/
readUseDownloadDir() {
var downloadFolder = document.getElementById("downloadFolder");
var chooseFolder = document.getElementById("chooseFolder");
var useDownloadDirPreference = Preferences.get(
"browser.download.useDownloadDir"
);
var dirPreference = Preferences.get("browser.download.dir");
downloadFolder.disabled =
!useDownloadDirPreference.value || dirPreference.locked;
chooseFolder.disabled =
!useDownloadDirPreference.value || dirPreference.locked;
this.readCloudStorage().catch(Cu.reportError);
// don't override the preference's value in UI
return undefined;
},
/**
* Show/Hide the cloud storage radio button with provider name as label if
* cloud storage provider is in use.
* Select cloud storage radio button if browser.download.useDownloadDir is true
* and browser.download.folderList has value 3. Enables/disables the folder field
* and Browse button if cloud storage radio button is selected.
*
*/
async readCloudStorage() {
// Get preferred provider in use display name
let providerDisplayName = await CloudStorage.getProviderIfInUse();
if (providerDisplayName) {
// Show cloud storage radio button with provider name in label
let saveToCloudRadio = document.getElementById("saveToCloud");
document.l10n.setAttributes(
saveToCloudRadio,
"save-files-to-cloud-storage",
{
"service-name": providerDisplayName,
}
);
saveToCloudRadio.hidden = false;
let useDownloadDirPref = Preferences.get(
"browser.download.useDownloadDir"
);
let folderListPref = Preferences.get("browser.download.folderList");
// Check if useDownloadDir is true and folderListPref is set to Cloud Storage value 3
// before selecting cloudStorageradio button. Disable folder field and Browse button if
// 'Save to Cloud Storage Provider' radio option is selected
if (useDownloadDirPref.value && folderListPref.value === 3) {
document.getElementById("saveWhere").selectedItem = saveToCloudRadio;
document.getElementById("downloadFolder").disabled = true;
document.getElementById("chooseFolder").disabled = true;
}
}
},
/**
* Handle clicks to 'Save To <custom path> or <system default downloads>' and
* 'Save to <cloud storage provider>' if cloud storage radio button is displayed in UI.
* Sets browser.download.folderList value and Enables/disables the folder field and Browse
* button based on option selected.
*/
handleSaveToCommand(event) {
return this.handleSaveToCommandTask(event).catch(Cu.reportError);
},
async handleSaveToCommandTask(event) {
if (event.target.id !== "saveToCloud" && event.target.id !== "saveTo") {
return;
}
// Check if Save To Cloud Storage Provider radio option is displayed in UI
// before continuing.
let saveToCloudRadio = document.getElementById("saveToCloud");
if (!saveToCloudRadio.hidden) {
// When switching between SaveTo and SaveToCloud radio button
// with useDownloadDirPref value true, if selectedIndex is other than
// SaveTo radio button disable downloadFolder filefield and chooseFolder button
let saveWhere = document.getElementById("saveWhere");
let useDownloadDirPref = Preferences.get(
"browser.download.useDownloadDir"
);
if (useDownloadDirPref.value) {
let downloadFolder = document.getElementById("downloadFolder");
let chooseFolder = document.getElementById("chooseFolder");
downloadFolder.disabled =
saveWhere.selectedIndex || useDownloadDirPref.locked;
chooseFolder.disabled =
saveWhere.selectedIndex || useDownloadDirPref.locked;
}
// Set folderListPref value depending on radio option
// selected. folderListPref should be set to 3 if Save To Cloud Storage Provider
// option is selected. If user switch back to 'Save To' custom path or system
// default Downloads, check pref 'browser.download.dir' before setting respective
// folderListPref value. If currentDirPref is unspecified folderList should
// default to 1
let folderListPref = Preferences.get("browser.download.folderList");
let saveTo = document.getElementById("saveTo");
if (saveWhere.selectedItem == saveToCloudRadio) {
folderListPref.value = 3;
} else if (saveWhere.selectedItem == saveTo) {
let currentDirPref = Preferences.get("browser.download.dir");
folderListPref.value = currentDirPref.value
? await this._folderToIndex(currentDirPref.value)
: 1;
}
}
},
/**
* 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(Cu.reportError);
},
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(Cu.reportError);
// don't override the preference's value in UI
return undefined;
},
async displayDownloadDirPrefTask() {
var folderListPref = Preferences.get("browser.download.folderList");
var downloadFolder = document.getElementById("downloadFolder");
var currentDirPref = Preferences.get("browser.download.dir");
// Used in defining the correct path to the folder icon.
var fph = Services.io
.getProtocolHandler("file")
.QueryInterface(Ci.nsIFileProtocolHandler);
var iconUrlSpec;
let folderIndex = folderListPref.value;
if (folderIndex == 3) {
// When user has selected cloud storage, use value in currentDirPref to
// compute index to display download folder label and icon to avoid
// displaying blank downloadFolder label and icon on load of preferences UI
// Set folderIndex to 1 if currentDirPref is unspecified
folderIndex = currentDirPref.value
? await this._folderToIndex(currentDirPref.value)
: 1;
}
// Display a 'pretty' label or the path in the UI.
// note: downloadFolder.value is not read elsewhere in the code, its only purpose is to display to the user
if (folderIndex == 2) {
// Force the left-to-right direction when displaying a custom path.
downloadFolder.value = currentDirPref.value
? `\u2066${currentDirPref.value.path}\u2069`
: "";
iconUrlSpec = fph.getURLSpecFromFile(currentDirPref.value);
} else if (folderIndex == 1) {
// 'Downloads'
[downloadFolder.value] = await document.l10n.formatValues([
{ id: "downloads-folder-name" },
]);
iconUrlSpec = fph.getURLSpecFromFile(await this._indexToFolder(1));
} else {
// 'Desktop'
[downloadFolder.value] = await document.l10n.formatValues([
{ id: "desktop-folder-name" },
]);
iconUrlSpec = fph.getURLSpecFromFile(
await this._getDownloadsFolder("Desktop")
);
}
downloadFolder.style.backgroundImage =
"url(moz-icon://" + iconUrlSpec + "?size=16)";
},
/**
* 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 flex="1" equalsize="always">
<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>
</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) {
Cu.reportError("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 plugins and 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;
// A plugin that can handle this type, if any.
//
// Note: just because we have one doesn't mean it *will* handle the type.
// That depends on whether or not the type is in the list of types for which
// plugin handling is disabled.
this.pluginName = "";
// Whether or not this type is only handled by a plugin or is also handled
// by some user-configured action as specified in the handler info object.
//
// Note: we can't just check if there's a handler info object for this type,
// because OS and user configuration is mixed up in the handler info object,
// so we always need to retrieve it for the OS info and can't tell whether
// it represents only OS-default information or user-configured information.
//
// FIXME: once handler info records are broken up into OS-provided records
// and user-configured records, stop using this boolean flag and simply
// check for the presence of a user-configured record to determine whether
// or not this type is only handled by a plugin. Filed as bug 395142.
this.handledOnlyByPlugin = 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;
case kActionUsePlugin:
return "plugin";
}
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 we have an enabled plugin, then the action is to use that plugin.
if (this.pluginName && !this.isDisabledPluginType) {
return kActionUsePlugin;
}
// 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) {
// If the action is to use the plugin,
// we must set the preferred action to "save to disk".
// But only if it's not currently the preferred action.
if (
aNewValue == kActionUsePlugin &&
this.preferredAction != Ci.nsIHandlerInfo.saveToDisk
) {
aNewValue = Ci.nsIHandlerInfo.saveToDisk;
}
// We don't modify the preferred action if the new action is to use a plugin
// because handler info objects don't understand our custom "use plugin"
// value. Also, leaving it untouched means that we can automatically revert
// to the old setting if the user ever removes the plugin.
if (aNewValue != kActionUsePlugin) {
this.wrappedHandlerInfo.preferredAction = aNewValue;
}
}
get alwaysAskBeforeHandling() {
// If this type is handled only by a plugin, we can't trust the value
// in the handler info object, since it'll be a default based on the absence
// of any user configuration, and the default in that case is to always ask,
// even though we never ask for content handled by a plugin, so special case
// plugin-handled types by returning false here.
if (this.pluginName && this.handledOnlyByPlugin) {
return false;
}
// 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.
//
// XXX Plugin objects contain an array of MimeType objects with "suffixes"
// properties; if this object has an associated plugin, shouldn't we check
// those properties for an extension?
get primaryExtension() {
try {
if (
this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
this.wrappedHandlerInfo.primaryExtension
) {
return this.wrappedHandlerInfo.primaryExtension;
}
} catch (ex) {}
return null;
}
get isDisabledPluginType() {
return this._getDisabledPluginTypes().includes(this.type);
}
_getDisabledPluginTypes() {
var types = "";
if (Services.prefs.prefHasUserValue(PREF_DISABLED_PLUGIN_TYPES)) {
types = Services.prefs.getCharPref(PREF_DISABLED_PLUGIN_TYPES);
}
// Only split if the string isn't empty so we don't end up with an array
// containing a single empty string.
if (types != "") {
return types.split(",");
}
return [];
}
disablePluginType() {
var disabledPluginTypes = this._getDisabledPluginTypes();
if (!disabledPluginTypes.includes(this.type)) {
disabledPluginTypes.push(this.type);
}
Services.prefs.setCharPref(
PREF_DISABLED_PLUGIN_TYPES,
disabledPluginTypes.join(",")
);
// Update the category manager so existing browser windows update.
Services.catMan.deleteCategoryEntry(
"Gecko-Content-Viewers",
this.type,
false
);
}
enablePluginType() {
var disabledPluginTypes = this._getDisabledPluginTypes();
var type = this.type;
disabledPluginTypes = disabledPluginTypes.filter(v => v != type);
Services.prefs.setCharPref(
PREF_DISABLED_PLUGIN_TYPES,
disabledPluginTypes.join(",")
);
// Update the category manager so existing browser windows update.
Services.catMan.addCategoryEntry(
"Gecko-Content-Viewers",
this.type,
"@mozilla.org/content/plugin/document-loader-factory;1",
false,
true
);
}
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 enabled() {
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
}
}
class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper {
constructor() {
super(TYPE_PDF, null);
}
get enabled() {
return !Services.prefs.getBoolPref(PREF_PDFJS_DISABLED);
}
}
class ViewableInternallyHandlerInfoWrapper extends InternalHandlerInfoWrapper {
constructor(extension) {
super(null, extension);
}
get enabled() {
return DownloadIntegration.shouldViewDownloadInternally(this.type);
}
}