diff options
Diffstat (limited to 'comm/mail/components/preferences')
62 files changed, 18120 insertions, 0 deletions
diff --git a/comm/mail/components/preferences/actionsshared.js b/comm/mail/components/preferences/actionsshared.js new file mode 100644 index 0000000000..aae709d957 --- /dev/null +++ b/comm/mail/components/preferences/actionsshared.js @@ -0,0 +1,23 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +var FILEACTION_SAVE_TO_DISK = 1; +var FILEACTION_OPEN_INTERNALLY = 2; +var FILEACTION_OPEN_DEFAULT = 3; +var FILEACTION_OPEN_CUSTOM = 4; +function FileAction() {} +FileAction.prototype = { + type: "", + extension: "", + hasExtension: true, + editable: true, + smallIcon: "", + bigIcon: "", + typeName: "", + action: "", + mimeInfo: null, + customHandler: "", + handleMode: false, +}; diff --git a/comm/mail/components/preferences/applicationManager.js b/comm/mail/components/preferences/applicationManager.js new file mode 100644 index 0000000000..5ebc0f077b --- /dev/null +++ b/comm/mail/components/preferences/applicationManager.js @@ -0,0 +1,112 @@ +/* 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/. */ + +// applications.js +/* globals gGeneralPane */ + +var gAppManagerDialog = { + _removed: [], + + init() { + this.handlerInfo = window.arguments[0]; + var bundle = document.getElementById("appManagerBundle"); + gGeneralPane._prefsBundle = document.getElementById("bundlePreferences"); + var description = this.handlerInfo.typeDescription; + var key = + this.handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo + ? "handleFile" + : "handleProtocol"; + var contentText = bundle.getFormattedString(key, [description]); + contentText = bundle.getFormattedString("descriptionApplications", [ + contentText, + ]); + document.getElementById("appDescription").textContent = contentText; + + let list = document.getElementById("appList"); + let listFragment = document.createDocumentFragment(); + for (let app of this.handlerInfo.possibleApplicationHandlers.enumerate()) { + if (!gGeneralPane.isValidHandlerApp(app)) { + continue; + } + + let item = document.createXULElement("richlistitem"); + item.classList.add("typeLabel"); + listFragment.append(item); + item.app = app; + + let image = document.createElement("img"); + image.classList.add("typeIcon"); + image.setAttribute("src", gGeneralPane._getIconURLForHandlerApp(app)); + image.setAttribute("alt", ""); + item.appendChild(image); + + let label = document.createElement("span"); + label.classList.add("typeDescription"); + label.textContent = app.name; + item.appendChild(label); + } + list.append(listFragment); + + // Triggers onSelect which populates label. + list.selectedIndex = 0; + }, + + onOK() { + if (!this._removed.length) { + // return early to avoid calling the |store| method. + return; + } + + for (var i = 0; i < this._removed.length; ++i) { + this.handlerInfo.removePossibleApplicationHandler(this._removed[i]); + } + + this.handlerInfo.store(); + }, + + remove() { + var list = document.getElementById("appList"); + this._removed.push(list.selectedItem.app); + var index = list.selectedIndex; + list.selectedItem.remove(); + if (list.getRowCount() == 0) { + // The list is now empty, make the bottom part disappear + document.getElementById("appDetails").hidden = true; + } else { + // Select the item at the same index, if we removed the last + // item of the list, select the previous item + if (index == list.getRowCount()) { + --index; + } + list.selectedIndex = index; + } + }, + + onSelect() { + var list = document.getElementById("appList"); + if (!list.selectedItem) { + document.getElementById("remove").disabled = true; + return; + } + document.getElementById("remove").disabled = false; + var app = list.selectedItem.app; + var address = ""; + if (app instanceof Ci.nsILocalHandlerApp) { + address = app.executable.path; + } else if (app instanceof Ci.nsIWebHandlerApp) { + address = app.uriTemplate; + } else if (app instanceof Ci.nsIWebContentHandlerInfo) { + address = app.uri; + } + document.getElementById("appLocation").value = address; + var bundle = document.getElementById("appManagerBundle"); + var appType = + app instanceof Ci.nsILocalHandlerApp + ? "descriptionLocalApp" + : "descriptionWebApp"; + document.getElementById("appType").value = bundle.getString(appType); + }, +}; + +document.addEventListener("dialogaccept", () => gAppManagerDialog.onOK()); diff --git a/comm/mail/components/preferences/applicationManager.xhtml b/comm/mail/components/preferences/applicationManager.xhtml new file mode 100644 index 0000000000..10c9346c0f --- /dev/null +++ b/comm/mail/components/preferences/applicationManager.xhtml @@ -0,0 +1,76 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?> +<?xml-stylesheet href="chrome://messenger/skin/preferences/applications.css"?> + +<!DOCTYPE window> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gAppManagerDialog.init();" + data-l10n-id="app-manager-window-dialog2" + style="min-width: 30em" +> + <dialog id="appManager" buttons="accept,cancel"> + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://messenger/content/preferences/general.js" /> + <script src="chrome://messenger/content/preferences/applicationManager.js" /> + + <commandset id="appManagerCommandSet"> + <command + id="cmd_delete" + oncommand="gAppManagerDialog.remove();" + disabled="true" + /> + </commandset> + + <keyset id="appManagerKeyset"> + <key id="delete" keycode="VK_DELETE" command="cmd_delete" /> + </keyset> + + <stringbundle + id="appManagerBundle" + src="chrome://messenger/locale/preferences/applicationManager.properties" + /> + <stringbundle + id="bundlePreferences" + src="chrome://messenger/locale/preferences/preferences.properties" + /> + + <linkset> + <html:link + rel="localization" + href="messenger/preferences/application-manager.ftl" + /> + </linkset> + + <description id="appDescription" /> + <separator class="thin" /> + <hbox flex="1"> + <richlistbox + id="appList" + onselect="gAppManagerDialog.onSelect();" + flex="1" + style="min-height: 150px" + /> + <vbox> + <button + id="remove" + data-l10n-id="remove-app-button" + command="cmd_delete" + /> + <spacer flex="1" /> + </vbox> + </hbox> + <vbox id="appDetails"> + <separator class="thin" /> + <label id="appType" /> + <html:input id="appLocation" type="text" readonly="readonly" /> + </vbox> + </dialog> +</window> diff --git a/comm/mail/components/preferences/attachmentReminder.js b/comm/mail/components/preferences/attachmentReminder.js new file mode 100644 index 0000000000..da01364b4e --- /dev/null +++ b/comm/mail/components/preferences/attachmentReminder.js @@ -0,0 +1,100 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +var gAttachmentReminderOptionsDialog = { + keywordListBox: null, + + init() { + this.keywordListBox = document.getElementById("keywordList"); + this.buildKeywordList(); + }, + + buildKeywordList() { + var keywordsInCsv = Services.prefs.getComplexValue( + "mail.compose.attachment_reminder_keywords", + Ci.nsIPrefLocalizedString + ); + if (!keywordsInCsv) { + return; + } + keywordsInCsv = keywordsInCsv.data; + var keywordsInArr = keywordsInCsv.split(","); + for (let i = 0; i < keywordsInArr.length; i++) { + if (keywordsInArr[i]) { + this.keywordListBox.appendItem(keywordsInArr[i], keywordsInArr[i]); + } + } + if (keywordsInArr.length) { + this.keywordListBox.selectedIndex = 0; + } + }, + + async addKeyword() { + var input = { value: "" }; // Default to empty. + + let [title, message] = await document.l10n.formatValues([ + { id: "new-keyword-title" }, + { id: "new-keyword-label" }, + ]); + + var ok = Services.prompt.prompt(window, title, message, input, null, { + value: 0, + }); + if (ok && input.value) { + let newKey = this.keywordListBox.appendItem(input.value, input.value); + this.keywordListBox.ensureElementIsVisible(newKey); + this.keywordListBox.selectItem(newKey); + } + }, + + async editKeyword() { + if (this.keywordListBox.selectedIndex < 0) { + return; + } + var keywordToEdit = this.keywordListBox.selectedItem; + var input = { value: keywordToEdit.getAttribute("value") }; + + let [title, message] = await document.l10n.formatValues([ + { id: "edit-keyword-title" }, + { id: "edit-keyword-label" }, + ]); + + var ok = Services.prompt.prompt(window, title, message, input, null, { + value: 0, + }); + if (ok && input.value) { + this.keywordListBox.selectedItem.value = input.value; + this.keywordListBox.selectedItem.label = input.value; + } + }, + + removeKeyword() { + if (this.keywordListBox.selectedIndex < 0) { + return; + } + this.keywordListBox.selectedItem.remove(); + }, + + saveKeywords() { + var keywordList = ""; + for (var i = 0; i < this.keywordListBox.getRowCount(); i++) { + keywordList += this.keywordListBox + .getItemAtIndex(i) + .getAttribute("value"); + if (i != this.keywordListBox.getRowCount() - 1) { + keywordList += ","; + } + } + + Services.prefs.setStringPref( + "mail.compose.attachment_reminder_keywords", + keywordList + ); + }, +}; + +document.addEventListener("dialogaccept", () => + gAttachmentReminderOptionsDialog.saveKeywords() +); diff --git a/comm/mail/components/preferences/attachmentReminder.xhtml b/comm/mail/components/preferences/attachmentReminder.xhtml new file mode 100644 index 0000000000..7f2f7b19ce --- /dev/null +++ b/comm/mail/components/preferences/attachmentReminder.xhtml @@ -0,0 +1,54 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<!DOCTYPE window> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="attachment-reminder-window" + onload="gAttachmentReminderOptionsDialog.init();" + style="min-width: 38em" +> + <dialog id="attachmentReminderOptionsDialog" dlgbuttons="accept,cancel"> + <script src="chrome://messenger/content/preferences/attachmentReminder.js" /> + + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link + rel="localization" + href="messenger/preferences/attachment-reminder.ftl" + /> + </linkset> + + <vbox> + <label control="keywordList" data-l10n-id="attachment-reminder-label" /> + <hbox> + <richlistbox + id="keywordList" + flex="1" + ondblclick="gAttachmentReminderOptionsDialog.editKeyword();" + /> + <vbox> + <button + data-l10n-id="keyword-new-button" + oncommand="gAttachmentReminderOptionsDialog.addKeyword();" + /> + <button + data-l10n-id="keyword-edit-button" + oncommand="gAttachmentReminderOptionsDialog.editKeyword();" + /> + <button + data-l10n-id="keyword-remove-button" + oncommand="gAttachmentReminderOptionsDialog.removeKeyword();" + /> + </vbox> + </hbox> + </vbox> + </dialog> +</window> diff --git a/comm/mail/components/preferences/chat.inc.xhtml b/comm/mail/components/preferences/chat.inc.xhtml new file mode 100644 index 0000000000..22b0cd3c4a --- /dev/null +++ b/comm/mail/components/preferences/chat.inc.xhtml @@ -0,0 +1,198 @@ +# 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/. + <script src="chrome://messenger/content/preferences/chat.js"/> + <script src="chrome://messenger/content/preferences/messagestyle.js"/> + + <stringbundle id="themesBundle" + src="chrome://messenger/locale/preferences/messagestyle.properties"/> + <html:template id="paneChat"> + <hbox id="chatPaneCategory" + class="subcategory" + data-category="paneChat"> + <html:h1 data-l10n-id="chat-pane-header"/> + </hbox> + + <html:div data-category="paneChat"> + <html:fieldset data-category="paneChat"> + <html:legend data-l10n-id="chat-status-title"></html:legend> + <!-- Startup --> + <hbox align="center"> + <label id="chatStartupAction" + data-l10n-id="startup-label" + control="messengerStartupAction"/> + <hbox> + <menulist id="messengerStartupAction" preference="messenger.startup.action"> + <menupopup> + <menuitem data-l10n-id="offline-label" value="0"/> + <menuitem data-l10n-id="auto-connect-label" value="1"/> + </menupopup> + </menulist> + </hbox> + </hbox> + <separator/> + + <!-- Status --> + <hbox align="center"> + <checkbox id="reportIdle" data-l10n-id="idle-label" + preference="messenger.status.reportIdle"/> + <html:input id="timeBeforeAway" type="number" + class="size2 idle-reporting-enabled" + min="1" max="720" + preference="messenger.status.timeBeforeIdle"/> + <label data-l10n-id="idle-time-label" control="timeBeforeAway"/> + </hbox> + <vbox class="indent"> + <hbox> + <checkbox id="autoAway" + data-l10n-id="away-message-label" + class="idle-reporting-enabled" + preference="messenger.status.awayWhenIdle"/> + <spacer flex="1"/> + </hbox> + <html:input id="defaultIdleAwayMessage" + type="text" + class="idle-reporting-enabled indent" + preference="messenger.status.defaultIdleAwayMessage"/> + </vbox> + </html:fieldset> + </html:div> + + <html:div data-category="paneChat"> + <html:fieldset data-category="paneChat"> + <html:legend data-l10n-id="chat-notifications-title"></html:legend> + <hbox> + <checkbox id="sendTyping" + data-l10n-id="send-typing-label" + preference="purple.conversations.im.send_typing"/> + <spacer flex="1"/> + </hbox> + + <separator/> + + <hbox> + <label data-l10n-id="notification-label"/> + </hbox> + <hbox> + <checkbox id="desktopChatNotifications" + data-l10n-id="show-notification-label" + preference="mail.chat.show_desktop_notifications"/> + <hbox> + <menulist id="chatNotificationInfo" preference="mail.chat.notification_info"> + <menupopup> + <menuitem data-l10n-id="notification-all" value="0"/> + <menuitem data-l10n-id="notification-name" value="1"/> + <menuitem data-l10n-id="notification-empty" value="2"/> + </menupopup> + </menulist> + </hbox> + </hbox> + <checkbox id="getAttention" + preference="messenger.options.getAttentionOnNewMessages" + data-l10n-id="notification-type-label"/> + <hbox align="center"> + <checkbox id="chatNotification" + data-l10n-id="chat-play-sound-label" + preference="mail.chat.play_sound"/> + <spacer flex="1"/> + <button is="highlightable-button" id="playChatSound" + data-l10n-id="chat-play-button" + oncommand="gChatPane.previewSound();"/> + </hbox> + <radiogroup id="chatSoundType" + class="indent" + orient="vertical" + preference="mail.chat.play_sound.type" + aria-labelledby="chatNotification"> + <hbox> + <radio id="chatSoundSystemSound" + data-l10n-id="chat-system-sound-label" + value="0"/> + <spacer flex="1"/> + </hbox> + <hbox> + <radio id="chatSoundCustom" + data-l10n-id="chat-custom-sound-label" + value="1"/> + <spacer flex="1"/> + </hbox> + <hbox align="center" class="input-container"> + <html:input id="chatSoundUrlLocation" + type="text" + class="input-filefield indent" + readonly="readonly" + preference="mail.chat.play_sound.url" + preference-editable="true" + aria-labelledby="chatSoundCustom"/> + <button is="highlightable-button" id="browseForChatSound" + data-l10n-id="chat-browse-sound-button" + oncommand="gChatPane.browseForSoundFile();"/> + </hbox> + </radiogroup> + </html:fieldset> + </html:div> + + <hbox id="chatPaneStylingCategory" + class="subcategory" + data-category="paneChat"> + <html:h1 data-l10n-id="chat-pane-styling-header"/> + </hbox> + + <html:div data-category="paneChat"> + <html:fieldset data-category="paneChat"> + <separator/> + <hbox align="center"> + <label data-l10n-id="theme-label" control="messagestyle-themename"/> + <hbox flex="1"> + <menulist id="messagestyle-themename" + flex="1" crop="end" + preference="messenger.options.messagesStyle.theme" + onselect="previewObserver.currentThemeChanged();"> + <menupopup id="theme-menupopup"> + <menuitem id="mail-menuitem" + data-l10n-id="style-mail" + value="mail"/> + <menuitem id="bubbles-menuitem" + data-l10n-id="style-bubbles" + value="bubbles"/> + <menuitem id="dark-menuitem" + data-l10n-id="style-dark" + value="dark"/> + <menuitem id="papersheets-menuitem" + data-l10n-id="style-paper" + value="papersheets"/> + <menuitem id="simple-menuitem" + data-l10n-id="style-simple" + value="simple"/> + </menupopup> + </menulist> + </hbox> + </hbox> + <separator class="thin"/> + <hbox align="start"> + <label data-l10n-id="preview-label"/> + <tooltip id="aHTMLTooltip" page="true"/> + <vbox id="previewBox" flex="1"> + <vbox id="noPreviewScreen" flex="1" align="center" pack="center"> + <hbox id="noPreviewBox" align="start"> + <vbox id="noPreviewInnerBox" flex="1"> + <label id="noPreviewTitle" data-l10n-id="no-preview-label"/> + <description id="noAccountDesc" + data-l10n-id="no-preview-description"/> + </vbox> + </hbox> + </vbox> + </vbox> + </hbox> + <hbox align="center"> + <label data-l10n-id="chat-variant-label" control="themevariant"/> + <hbox> + <menulist id="themevariant" + preference="messenger.options.messagesStyle.variant" + onselect="previewObserver.currentVariantChanged();"/> + </hbox> + </hbox> + </html:fieldset> + </html:div> + + </html:template> diff --git a/comm/mail/components/preferences/chat.js b/comm/mail/components/preferences/chat.js new file mode 100644 index 0000000000..e6e7b660c8 --- /dev/null +++ b/comm/mail/components/preferences/chat.js @@ -0,0 +1,193 @@ +/* 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/. */ + +"use strict"; + +/* import-globals-from preferences.js */ +// messagestyle.js +/* globals previewObserver */ + +Preferences.addAll([ + { id: "messenger.startup.action", type: "int" }, + { id: "purple.conversations.im.send_typing", type: "bool" }, + { id: "messenger.status.reportIdle", type: "bool" }, + { id: "messenger.status.timeBeforeIdle", type: "int" }, + { id: "messenger.status.awayWhenIdle", type: "bool" }, + { id: "messenger.status.defaultIdleAwayMessage", type: "wstring" }, + { id: "purple.logging.log_chats", type: "bool" }, + { id: "purple.logging.log_ims", type: "bool" }, + { id: "purple.logging.log_system", type: "bool" }, + { id: "mail.chat.show_desktop_notifications", type: "bool" }, + { id: "mail.chat.notification_info", type: "int" }, + { id: "mail.chat.play_sound", type: "bool" }, + { id: "mail.chat.play_sound.type", type: "int" }, + { id: "mail.chat.play_sound.url", type: "string" }, + { id: "messenger.options.getAttentionOnNewMessages", type: "bool" }, + { id: "messenger.options.messagesStyle.theme", type: "string" }, + { id: "messenger.options.messagesStyle.variant", type: "string" }, +]); + +var gChatPane = { + init() { + this.updateDisabledState(); + this.updateMessageDisabledState(); + this.updatePlaySound(); + this.initPreview(); + + let element = document.getElementById("timeBeforeAway"); + Preferences.addSyncFromPrefListener( + element, + () => + Preferences.get("messenger.status.timeBeforeIdle") + .valueFromPreferences / 60 + ); + Preferences.addSyncToPrefListener(element, element => element.value * 60); + Preferences.addSyncFromPrefListener( + document.getElementById("chatSoundUrlLocation"), + () => this.readSoundLocation() + ); + }, + + initPreview() { + // We add this browser only when really necessary. + let previewBox = document.getElementById("previewBox"); + if (previewBox.querySelector("browser")) { + return; + } + + document.getElementById("noPreviewScreen").hidden = true; + let browser = document.createXULElement("browser", { + is: "conversation-browser", + }); + browser.setAttribute("id", "previewbrowser"); + browser.setAttribute("type", "content"); + browser.setAttribute("flex", "1"); + browser.setAttribute("tooltip", "aHTMLTooltip"); + previewBox.appendChild(browser); + previewObserver.load(); + }, + + updateDisabledState() { + let checked = Preferences.get("messenger.status.reportIdle").value; + document.querySelectorAll(".idle-reporting-enabled").forEach(e => { + e.disabled = !checked; + }); + }, + + updateMessageDisabledState() { + let textbox = document.getElementById("defaultIdleAwayMessage"); + textbox.toggleAttribute( + "disabled", + !Preferences.get("messenger.status.awayWhenIdle").value + ); + }, + + convertURLToLocalFile(aFileURL) { + // convert the file url into a nsIFile + if (aFileURL) { + return Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler) + .getFileFromURLSpec(aFileURL); + } + return null; + }, + + readSoundLocation() { + let chatSoundUrlLocation = document.getElementById("chatSoundUrlLocation"); + chatSoundUrlLocation.value = Preferences.get( + "mail.chat.play_sound.url" + ).value; + if (chatSoundUrlLocation.value) { + chatSoundUrlLocation.label = this.convertURLToLocalFile( + chatSoundUrlLocation.value + ).leafName; + chatSoundUrlLocation.style.backgroundImage = + "url(moz-icon://" + chatSoundUrlLocation.label + "?size=16)"; + } + }, + + previewSound() { + let sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound); + + let soundLocation = + document.getElementById("chatSoundType").value == 1 + ? document.getElementById("chatSoundUrlLocation").value + : ""; + + // This should be in sync with the code in nsStatusBarBiffManager::PlayBiffSound. + if (!soundLocation.startsWith("file://")) { + if (Services.appinfo.OS == "Darwin") { + // OS X + sound.beep(); + } else { + sound.playEventSound(Ci.nsISound.EVENT_NEW_MAIL_RECEIVED); + } + } else { + sound.play(Services.io.newURI(soundLocation)); + } + }, + + browseForSoundFile() { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + + // If we already have a sound file, then use the path for that sound file + // as the initial path in the dialog. + let localFile = this.convertURLToLocalFile( + document.getElementById("chatSoundUrlLocation").value + ); + if (localFile) { + fp.displayDirectory = localFile.parent; + } + + // XXX todo, persist the last sound directory and pass it in + fp.init( + window, + document + .getElementById("bundlePreferences") + .getString("soundFilePickerTitle"), + Ci.nsIFilePicker.modeOpen + ); + fp.appendFilters(Ci.nsIFilePicker.filterAudio); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + + fp.open(rv => { + if (rv != Ci.nsIFilePicker.returnOK) { + return; + } + + // convert the nsIFile into a nsIFile url + Preferences.get("mail.chat.play_sound.url").value = fp.fileURL.spec; + this.readSoundLocation(); // XXX We shouldn't have to be doing this by hand + this.updatePlaySound(); + }); + }, + + updatePlaySound() { + let soundsEnabled = Preferences.get("mail.chat.play_sound").value; + let soundTypeValue = Preferences.get("mail.chat.play_sound.type").value; + let soundUrlLocation = Preferences.get("mail.chat.play_sound.url").value; + let soundDisabled = !soundsEnabled || soundTypeValue != 1; + + document.getElementById("chatSoundType").disabled = !soundsEnabled; + document.getElementById("chatSoundUrlLocation").disabled = soundDisabled; + document.getElementById("browseForChatSound").disabled = soundDisabled; + document.getElementById("playChatSound").disabled = + !soundsEnabled || (!soundUrlLocation && soundTypeValue != 0); + }, +}; + +Preferences.get("messenger.status.reportIdle").on( + "change", + gChatPane.updateDisabledState +); +Preferences.get("messenger.status.awayWhenIdle").on( + "change", + gChatPane.updateMessageDisabledState +); +Preferences.get("mail.chat.play_sound").on("change", gChatPane.updatePlaySound); +Preferences.get("mail.chat.play_sound.type").on( + "change", + gChatPane.updatePlaySound +); diff --git a/comm/mail/components/preferences/colors.js b/comm/mail/components/preferences/colors.js new file mode 100644 index 0000000000..cd45f8ca44 --- /dev/null +++ b/comm/mail/components/preferences/colors.js @@ -0,0 +1,15 @@ +/* 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 ../../../../toolkit/content/preferencesBindings.js */ + +Preferences.addAll([ + { id: "browser.display.document_color_use", type: "int" }, + { id: "browser.anchor_color", type: "string" }, + { id: "browser.visited_color", type: "string" }, + { id: "browser.underline_anchors", type: "bool" }, + { id: "browser.display.foreground_color", type: "string" }, + { id: "browser.display.background_color", type: "string" }, + { id: "browser.display.use_system_colors", type: "bool" }, +]); diff --git a/comm/mail/components/preferences/colors.xhtml b/comm/mail/components/preferences/colors.xhtml new file mode 100644 index 0000000000..827078ba4c --- /dev/null +++ b/comm/mail/components/preferences/colors.xhtml @@ -0,0 +1,90 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<!DOCTYPE window> + +<window type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="colors-dialog-window2"> + <dialog id="ColorsDialog" + dlgbuttons="accept,cancel"> + + <linkset> + <html:link rel="localization" href="messenger/preferences/colors.ftl"/> + </linkset> + + <hbox> + <hbox flex="1"> + <html:div> + <html:fieldset> + <html:legend data-l10n-id="colors-dialog-legend"></html:legend> + <hbox align="center"> + <label data-l10n-id="text-color-label" control="foregroundtextmenu"/> + <spacer flex="1"/> + <html:input type="color" id="foregroundtextmenu" preference="browser.display.foreground_color"/> + </hbox> + <hbox align="center" style="margin-top: 5px"> + <label data-l10n-id="background-color-label" control="backgroundmenu"/> + <spacer flex="1"/> + <html:input type="color" id="backgroundmenu" preference="browser.display.background_color"/> + </hbox> + <separator class="thin"/> + <hbox align="center"> + <checkbox id="browserUseSystemColors" data-l10n-id="use-system-colors" + preference="browser.display.use_system_colors"/> + </hbox> + </html:fieldset> + </html:div> + </hbox> + <hbox flex="1"> + <html:div> + <html:fieldset> + <html:legend data-l10n-id="colors-link-legend"></html:legend> + <hbox align="center"> + <label data-l10n-id="link-color-label" control="unvisitedlinkmenu"/> + <spacer flex="1"/> + <html:input type="color" id="unvisitedlinkmenu" preference="browser.anchor_color"/> + </hbox> + <hbox align="center" style="margin-top: 5px"> + <label data-l10n-id="visited-link-color-label" control="visitedlinkmenu"/> + <spacer flex="1"/> + <html:input type="color" id="visitedlinkmenu" preference="browser.visited_color"/> + </hbox> + <separator class="thin"/> + <hbox align="center"> + <checkbox id="browserUnderlineAnchors" data-l10n-id="underline-link-checkbox" + preference="browser.underline_anchors"/> + </hbox> + </html:fieldset> + </html:div> + </hbox> + </hbox> +#ifdef XP_WIN + <vbox align="start"> +#else + <vbox> +#endif + <label data-l10n-id="override-color-label" + control="useDocumentColors"/> + <menulist id="useDocumentColors" + preference="browser.display.document_color_use"> + <menupopup> + <menuitem data-l10n-id="override-color-always" + value="2" id="documentColorAlways"/> + <menuitem data-l10n-id="override-color-auto" + value="0" id="documentColorAutomatic"/> + <menuitem data-l10n-id="override-color-never" + value="1" id="documentColorNever"/> + </menupopup> + </menulist> + </vbox> + + <script src="chrome://global/content/preferencesBindings.js"/> + <script src="chrome://messenger/content/preferences/colors.js"/> + </dialog> +</window> diff --git a/comm/mail/components/preferences/compose.inc.xhtml b/comm/mail/components/preferences/compose.inc.xhtml new file mode 100644 index 0000000000..3ba7063084 --- /dev/null +++ b/comm/mail/components/preferences/compose.inc.xhtml @@ -0,0 +1,354 @@ +# 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/. + <script src="chrome://messenger/content/preferences/compose.js"/> + <script src="chrome://global/content/contentAreaUtils.js"/> + <script src="chrome://messenger/content/preferences/downloads.js"/> + <script src="chrome://communicator/content/utilityOverlay.js"/> + + <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/> + <html:template id="paneCompose"> + <hbox id="compositionMainCategory" + class="subcategory" + data-category="paneCompose"> + <html:h1 data-l10n-id="composition-category-header"/> + </hbox> + + <html:div data-category="paneCompose"> + <html:fieldset data-category="paneCompose"> + <separator class="thin"/> + <hbox align="center"> + <label data-l10n-id="forward-label" control="forwardMessageMode"/> + <hbox> + <menulist id="forwardMessageMode" preference="mail.forward_message_mode"> + <menupopup> + <menuitem value="2" data-l10n-id="inline-label"/> + <menuitem value="0" data-l10n-id="as-attachment-label"/> + </menupopup> + </menulist> + </hbox> + <separator orient="vertical" class="thin"/> + <checkbox id="addExtension" preference="mail.forward_add_extension" + data-l10n-id="extension-label"/> + </hbox> + + <separator class="thin"/> + + <hbox align="center" pack="start"> + <checkbox id="autoSave" preference="mail.compose.autosave" + data-l10n-id="auto-save-label"/> + <html:input id="autoSaveInterval" type="number" class="size2" + min="1" max="35790" + preference="mail.compose.autosaveinterval" + aria-labelledby="autoSave autoSaveInterval autoSaveEnd"/> + <label id="autoSaveEnd" data-l10n-id="auto-save-end"/> + </hbox> + <hbox> + <checkbox id="mailWarnOnSendAccelKey" + data-l10n-id="warn-on-send-accel-key" + preference="mail.warn_on_send_accel_key"/> + <spacer flex="1"/> + </hbox> + <hbox> + <checkbox id="addLinkPreviews" + data-l10n-id="add-link-previews" + preference="mail.compose.add_link_preview"/> + <spacer flex="1"/> + </hbox> + </html:fieldset> + </html:div> + + <html:div data-category="paneCompose"> + <html:fieldset data-category="paneCompose"> + <html:legend data-l10n-id="composition-spelling-title"></html:legend> + <hbox> + <checkbox id="spellCheckBeforeSend" + data-l10n-id="spellcheck-label" + preference="mail.SpellCheckBeforeSend"/> + <spacer flex="1"/> + </hbox> + <hbox> + <checkbox id="inlineSpellCheck" + data-l10n-id="spellcheck-inline-label" + preference="mail.spellcheck.inline"/> + <spacer flex="1"/> + </hbox> + + <separator class="thin"/> + + <vbox flex="1"> + <label data-l10n-id="language-popup-label" control="dictionaryList"/> + + <html:ul id="dictionaryList" class="indent"> + <html:template id="dictionaryListItem"> + <html:li> + <html:label> + <html:input type="checkbox" /> + <html:span class="checkbox-label"></html:span> + </html:label> + </html:li> + </html:template> + </html:ul> + + <label id="downloadDictionaries" class="text-link" + onclick="if (event.button == 0) { openDictionaryList('tab'); }" + data-l10n-id="download-dictionaries-link"/> + + <spacer flex="1"/> + </vbox> + </html:fieldset> + </html:div> + + <html:div data-category="paneCompose"> + <html:fieldset data-category="paneCompose"> + <html:legend data-l10n-id="compose-html-style-title"></html:legend> + <hbox> + <vbox flex="1"> + <hbox align="center"> + <label control="FontSelect" data-l10n-id="font-label"/> + <hbox flex="1"> + <menulist id="FontSelect" preference="msgcompose.font_face" + sizetopopup="pref" crop="center" flex="1"> + <menupopup> + <menuitem value="" label="&fontVarWidth.label;"/> + <menuitem value="monospace" label="&fontFixedWidth.label;"/> + <menuseparator/> + <menuitem value="Helvetica, Arial, sans-serif" label="&fontHelvetica.label;"/> + <menuitem value="Times New Roman, Times, serif" label="&fontTimes.label;"/> + <menuitem value="Courier New, Courier, monospace" label="&fontCourier.label;"/> + <menuseparator/> + </menupopup> + </menulist> + </hbox> + + <label control="fontSizeSelect" data-l10n-id="font-size-label"/> + <hbox> + <menulist id="fontSizeSelect" preference="msgcompose.font_size" value="3"> + <menupopup> + <menuitem value="1" label="&size-tinyCmd.label;"/> + <menuitem value="2" label="&size-smallCmd.label;"/> + <menuitem value="3" label="&size-mediumCmd.label;"/> + <menuitem value="4" label="&size-largeCmd.label;"/> + <menuitem value="5" label="&size-extraLargeCmd.label;"/> + <menuitem value="6" label="&size-hugeCmd.label;"/> + </menupopup> + </menulist> + </hbox> + </hbox> + + <separator class="thin"/> + + <hbox align="center"> + <checkbox id="useReaderDefaults" + data-l10n-id="default-colors-label" + preference="msgcompose.default_colors"/> + </hbox> + <hbox align="center" class="indent"> + <label id="textColorLabel" + control="textColorButton" + data-l10n-id="font-color-label"/> + <html:input type="color" id="textColorButton" preference="msgcompose.text_color"/> + <separator orient="vertical" class="thin"/> + <label id="backgroundColorLabel" + control="backgroundColorButton" + data-l10n-id="bg-color-label"/> + <html:input type="color" id="backgroundColorButton" preference="msgcompose.background_color"/> + </hbox> + </vbox> + <vbox> + <spacer flex="1"/> + <button is="highlightable-button" + data-l10n-id="restore-html-label" + oncommand="gComposePane.restoreHTMLDefaults();"/> + </vbox> + </hbox> + + <separator class="thin"/> + + <hbox align="center"> + <checkbox id="defaultToParagraph" + data-l10n-id="default-format-label" + preference="mail.compose.default_to_paragraph"/> + </hbox> + </html:fieldset> + </html:div> + + <html:div data-category="paneCompose"> + <html:fieldset data-category="paneCompose"> + <html:legend data-l10n-id="compose-send-format-title"></html:legend> + <radiogroup class="indent" + preference="mail.default_send_format"> + <radio value="0" + aria-describedby="composeSendAutomaticDescription" + data-l10n-id="compose-send-automatic-option" /> + <description id="composeSendAutomaticDescription" + class="indent tip-caption" + data-l10n-id="compose-send-automatic-description" /> + <radio value="3" + aria-describedby="composeSendBothDescription" + data-l10n-id="compose-send-both-option" /> + <description id="composeSendBothDescription" + class="indent tip-caption" + data-l10n-id="compose-send-both-description" /> + <radio value="2" + aria-describedby="composeSendHTMLDescription" + data-l10n-id="compose-send-html-option" /> + <description id="composeSendHTMLDescription" + class="indent tip-caption" + data-l10n-id="compose-send-html-description" /> + <radio value="1" + aria-describedby="composeSendPlainDescription" + data-l10n-id="compose-send-plain-option" /> + <description id="composeSendPlainDescription" + class="indent tip-caption" + data-l10n-id="compose-send-plain-description" /> + </radiogroup> + </html:fieldset> + </html:div> + + <hbox id="compositionAddressingCategory" + class="subcategory" + data-category="paneCompose"> + <html:h1 data-l10n-id="composition-addressing-header"/> + </hbox> + + <html:div data-category="paneCompose"> + <html:fieldset data-category="paneCompose"> + <!-- Address Autocomplete --> + <separator class="thin"/> + + <description data-l10n-id="autocomplete-description"/> + + <hbox align="center"> + <checkbox id="addressingAutocomplete" data-l10n-id="ab-label" + preference="mail.enable_autocomplete"/> + </hbox> + + <hbox align="center"> + <checkbox id="autocompleteLDAP" data-l10n-id="directories-label" + preference="ldap_2.autoComplete.useDirectory"/> + <hbox flex="1"> + <menulist is="menulist-addrbooks" id="directoriesList" + aria-labelledby="autocompleteLDAP" + preference="ldap_2.autoComplete.directoryServer" + data-l10n-id="directories-none-label" + data-l10n-attrs="none" + remoteonly="true" + flex="1"/> + </hbox> + <button is="highlightable-button" id="editButton" + data-l10n-id="edit-directories-label" + oncommand="gComposePane.editDirectories();" + preference="pref.ldap.disable_button.edit_directories"/> + </hbox> + + <separator class="thin"/> + + <hbox align="center" pack="start"> + <checkbox id="emailCollectionOutgoing" data-l10n-id="email-picker-label" + preference="mail.collect_email_address_outgoing"/> + <hbox flex="1"> + <menulist is="menulist-addrbooks" id="localDirectoriesList" + aria-labelledby="emailCollectionOutgoing" + preference="mail.collect_addressbook" + localonly="true" + writable="true" + flex="1"/> + </hbox> + </hbox> + + <hbox align="center" pack="start"> + <label data-l10n-id="default-directory-label" + control="defaultStartupDirList"/> + <hbox flex="1"> + <menulist is="menulist-addrbooks" id="defaultStartupDirList" + oncommand="gComposePane.setDefaultStartupDir(this.value);" + data-l10n-id="default-last-label" + data-l10n-attrs="none" + alladdressbooks="true" + mailinglists="true" + flex="1"/> + </hbox> + </hbox> + </html:fieldset> + </html:div> + + <hbox id="compositionAttachmentsCategory" + class="subcategory" + data-category="paneCompose"> + <html:h1 data-l10n-id="composition-attachments-header"/> + </hbox> + + <html:div data-category="paneCompose"> + <html:fieldset data-category="paneCompose"> + <hbox align="center"> + <checkbox id="attachment_reminder_label" + data-l10n-id="attachment-label" + preference="mail.compose.attachment_reminder"/> + <spacer flex="1"/> + <hbox> + <button is="highlightable-button" id="attachment_reminder_button" + data-l10n-id="attachment-options-label" + oncommand="gComposePane.attachmentReminderOptionsDialog();" + search-l10n-ids=" + attachment-reminder-window.title, + attachment-reminder-label, + keyword-new-button.label, + keyword-edit-button.label, + keyword-remove-button.label, + new-keyword-title, + new-keyword-label, + edit-keyword-title, + edit-keyword-label"/> + </hbox> + </hbox> + <vbox id="cloudFileBox"> + <hbox id="cloudFileToggleAndThreshold" align="center"> + <checkbox id="enableThreshold" + data-l10n-id="enable-cloud-share" + preference="mail.compose.big_attachments.notify"/> + <html:input id="cloudFileThreshold" type="number" min="0" class="size3" + preference="mail.compose.big_attachments.threshold_kb"/> + <label control="cloudFileThreshold" data-l10n-id="cloud-share-size"/> + </hbox> + <hbox style="height: 480px; flex: 1 auto;"> + <vbox id="provider-listing"> + <richlistbox id="cloudFileView" orient="vertical" + seltype="single" + onoverflow="gCloudFile.onListOverflow();" + onselect="gCloudFile.onSelectionChanged(event);"> + </richlistbox> + <vbox id="addCloudFileAccountButtons"> + </vbox> + <hbox> + <menulist id="addCloudFileAccount" + hidden="true" + data-l10n-id="add-cloud-account" + data-l10n-attrs="defaultlabel" + oncommand="gCloudFile.addCloudFileAccount(this.value);"> + <menupopup id="addCloudFileAccountListItems"/> + </menulist> + </hbox> + <button is="highlightable-button" id="removeCloudFileAccount" + disabled="true" + data-l10n-id="remove-cloud-account" + oncommand="gCloudFile.removeCloudFileAccount();"/> + <label is="text-link" + id="moreProvidersLink" + href="https://addons.thunderbird.net/thunderbird/tag/filelink" + data-l10n-id="find-cloud-providers"/> + </vbox> + <separator class="thin" orient="vertical"/> + <vbox flex="1"> + <vbox id="cloudFileDefaultPanel" flex="1"> + <description data-l10n-id="cloud-account-description"/> + </vbox> + <vbox id="cloudFileSettingsWrapper" flex="1"> + </vbox> + </vbox> + </hbox> + </vbox> + </html:fieldset> + </html:div> + + </html:template> diff --git a/comm/mail/components/preferences/compose.js b/comm/mail/components/preferences/compose.js new file mode 100644 index 0000000000..7d9acf0622 --- /dev/null +++ b/comm/mail/components/preferences/compose.js @@ -0,0 +1,776 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +"use strict"; + +/* import-globals-from preferences.js */ + +var { InlineSpellChecker } = ChromeUtils.importESModule( + "resource://gre/modules/InlineSpellChecker.sys.mjs" +); + +// CloudFile account tools used by gCloudFile. +var { cloudFileAccounts } = ChromeUtils.import( + "resource:///modules/cloudFileAccounts.jsm" +); +var { E10SUtils } = ChromeUtils.importESModule( + "resource://gre/modules/E10SUtils.sys.mjs" +); +var { ExtensionParent } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionParent.sys.mjs" +); + +Preferences.addAll([ + { id: "mail.forward_message_mode", type: "int" }, + { id: "mail.forward_add_extension", type: "bool" }, + { id: "mail.SpellCheckBeforeSend", type: "bool" }, + { id: "mail.spellcheck.inline", type: "bool" }, + { id: "mail.warn_on_send_accel_key", type: "bool" }, + { id: "mail.compose.autosave", type: "bool" }, + { id: "mail.compose.autosaveinterval", type: "int" }, + { id: "mail.enable_autocomplete", type: "bool" }, + { id: "ldap_2.autoComplete.useDirectory", type: "bool" }, + { id: "ldap_2.autoComplete.directoryServer", type: "string" }, + { id: "pref.ldap.disable_button.edit_directories", type: "bool" }, + { id: "mail.collect_email_address_outgoing", type: "bool" }, + { id: "mail.collect_addressbook", type: "string" }, + { id: "spellchecker.dictionary", type: "unichar" }, + { id: "msgcompose.default_colors", type: "bool" }, + { id: "msgcompose.font_face", type: "string" }, + { id: "msgcompose.font_size", type: "string" }, + { id: "msgcompose.text_color", type: "string" }, + { id: "msgcompose.background_color", type: "string" }, + { id: "mail.compose.attachment_reminder", type: "bool" }, + { id: "mail.compose.default_to_paragraph", type: "bool" }, + { id: "mail.compose.big_attachments.notify", type: "bool" }, + { id: "mail.compose.big_attachments.threshold_kb", type: "int" }, + { id: "mail.default_send_format", type: "int" }, + { id: "mail.compose.add_link_preview", type: "bool" }, +]); + +var gComposePane = { + mSpellChecker: null, + + init() { + this.enableAutocomplete(); + + this.initLanguages(); + + this.populateFonts(); + + this.updateAutosave(); + + this.updateUseReaderDefaults(); + + this.updateAttachmentCheck(); + + this.updateEmailCollection(); + + this.initAbDefaultStartupDir(); + + this.setButtonColors(); + + // If BigFiles is disabled, hide the "Outgoing" tab, and the tab + // selectors, and bail out. + if (!Services.prefs.getBoolPref("mail.cloud_files.enabled")) { + // Hide the tab selector + let cloudFileBox = document.getElementById("cloudFileBox"); + cloudFileBox.hidden = true; + return; + } + + gCloudFile.init(); + }, + + attachmentReminderOptionsDialog() { + gSubDialog.open( + "chrome://messenger/content/preferences/attachmentReminder.xhtml", + { features: "resizable=no" } + ); + }, + + updateAutosave() { + gComposePane.enableElement( + document.getElementById("autoSaveInterval"), + Preferences.get("mail.compose.autosave").value + ); + }, + + updateUseReaderDefaults() { + let useReaderDefaultsChecked = Preferences.get( + "msgcompose.default_colors" + ).value; + gComposePane.enableElement( + document.getElementById("textColorLabel"), + !useReaderDefaultsChecked + ); + gComposePane.enableElement( + document.getElementById("backgroundColorLabel"), + !useReaderDefaultsChecked + ); + gComposePane.enableElement( + document.getElementById("textColorButton"), + !useReaderDefaultsChecked + ); + gComposePane.enableElement( + document.getElementById("backgroundColorButton"), + !useReaderDefaultsChecked + ); + }, + + updateAttachmentCheck() { + gComposePane.enableElement( + document.getElementById("attachment_reminder_button"), + Preferences.get("mail.compose.attachment_reminder").value + ); + }, + + updateEmailCollection() { + gComposePane.enableElement( + document.getElementById("localDirectoriesList"), + Preferences.get("mail.collect_email_address_outgoing").value + ); + }, + + enableElement(aElement, aEnable) { + let pref = aElement.getAttribute("preference"); + let prefIsLocked = pref ? Preferences.get(pref).locked : false; + aElement.disabled = !aEnable || prefIsLocked; + }, + + enableAutocomplete() { + let acLDAPPref = Preferences.get("ldap_2.autoComplete.useDirectory").value; + gComposePane.enableElement( + document.getElementById("directoriesList"), + acLDAPPref + ); + gComposePane.enableElement( + document.getElementById("editButton"), + acLDAPPref + ); + }, + + editDirectories() { + gSubDialog.open( + "chrome://messenger/content/addressbook/pref-editdirectories.xhtml" + ); + }, + + initAbDefaultStartupDir() { + if (!this.startupDirListener.inited) { + this.startupDirListener.load(); + } + + let dirList = document.getElementById("defaultStartupDirList"); + if (Services.prefs.getBoolPref("mail.addr_book.view.startupURIisDefault")) { + // Some directory is the default. + let startupURI = Services.prefs.getCharPref( + "mail.addr_book.view.startupURI" + ); + dirList.value = startupURI; + } else { + // Choose item meaning there is no default startup directory any more. + dirList.value = ""; + } + }, + + setButtonColors() { + document.getElementById("textColorButton").value = Preferences.get( + "msgcompose.text_color" + ).value; + document.getElementById("backgroundColorButton").value = Preferences.get( + "msgcompose.background_color" + ).value; + }, + + setDefaultStartupDir(aDirURI) { + if (aDirURI) { + // Some AB directory was selected. Set prefs to make this directory + // the default view when starting up the main AB. + Services.prefs.setCharPref("mail.addr_book.view.startupURI", aDirURI); + Services.prefs.setBoolPref( + "mail.addr_book.view.startupURIisDefault", + true + ); + } else { + // Set pref that there's no default startup view directory any more. + Services.prefs.setBoolPref( + "mail.addr_book.view.startupURIisDefault", + false + ); + } + }, + + async initLanguages() { + let languageList = document.getElementById("dictionaryList"); + this.mSpellChecker = Cc["@mozilla.org/spellchecker/engine;1"].getService( + Ci.mozISpellCheckingEngine + ); + + // Get the list of dictionaries from the spellchecker. + + let dictList = this.mSpellChecker.getDictionaryList(); + + // HACK: calling sortDictionaryList may fail the first time due to + // synchronous loading of the .ftl files. If we load the files and wait + // for a known value asynchronously, no such failure will happen. + await new Localization([ + "toolkit/intl/languageNames.ftl", + "toolkit/intl/regionNames.ftl", + ]).formatValue("language-name-en"); + let sortedList = new InlineSpellChecker().sortDictionaryList(dictList); + let activeDictionaries = Services.prefs + .getCharPref("spellchecker.dictionary") + .split(","); + let template = document.getElementById("dictionaryListItem"); + languageList.replaceChildren( + ...sortedList.map(({ displayName, localeCode }) => { + let item = template.content.cloneNode(true).firstElementChild; + item.querySelector(".checkbox-label").textContent = displayName; + let input = item.querySelector("input"); + input.setAttribute("value", localeCode); + input.addEventListener("change", event => { + let language = event.target.value; + let dicts = Services.prefs + .getCharPref("spellchecker.dictionary") + .split(",") + .filter(Boolean); + if (!event.target.checked) { + dicts = dicts.filter(item => item != language); + } else { + dicts.push(language); + } + Services.prefs.setCharPref( + "spellchecker.dictionary", + dicts.join(",") + ); + }); + input.checked = activeDictionaries.includes(localeCode); + return item; + }) + ); + }, + + populateFonts() { + var fontsList = document.getElementById("FontSelect"); + try { + var enumerator = Cc["@mozilla.org/gfx/fontenumerator;1"].getService( + Ci.nsIFontEnumerator + ); + var localFonts = enumerator.EnumerateAllFonts(); + for (let i = 0; i < localFonts.length; ++i) { + // Remove Linux system generic fonts that collide with CSS generic fonts. + if ( + localFonts[i] != "" && + localFonts[i] != "serif" && + localFonts[i] != "sans-serif" && + localFonts[i] != "monospace" + ) { + fontsList.appendItem(localFonts[i], localFonts[i]); + } + } + } catch (e) {} + // Choose the item after the list is completely generated. + var preference = Preferences.get(fontsList.getAttribute("preference")); + fontsList.value = preference.value; + }, + + restoreHTMLDefaults() { + // reset throws an exception if the pref value is already the default so + // work around that with some try/catch exception handling + try { + Preferences.get("msgcompose.font_face").reset(); + } catch (ex) {} + + try { + Preferences.get("msgcompose.font_size").reset(); + } catch (ex) {} + + try { + Preferences.get("msgcompose.text_color").reset(); + } catch (ex) {} + + try { + Preferences.get("msgcompose.background_color").reset(); + } catch (ex) {} + + try { + Preferences.get("msgcompose.default_colors").reset(); + } catch (ex) {} + + this.updateUseReaderDefaults(); + this.setButtonColors(); + }, + + startupDirListener: { + inited: false, + domain: "mail.addr_book.view.startupURI", + observe(subject, topic, prefName) { + if (topic != "nsPref:changed") { + return; + } + + // If the default startup directory prefs have changed, + // reinitialize the default startup dir picker to show the new value. + gComposePane.initAbDefaultStartupDir(); + }, + load() { + // Observe changes of our prefs. + Services.prefs.addObserver(this.domain, this); + // Unload the pref observer when preferences window is closed. + window.addEventListener("unload", () => this.unload(), true); + this.inited = true; + }, + + unload(event) { + Services.prefs.removeObserver( + gComposePane.startupDirListener.domain, + gComposePane.startupDirListener + ); + }, + }, +}; + +var gCloudFile = { + _initialized: false, + _list: null, + _buttonContainer: null, + _listContainer: null, + _settings: null, + _tabpanel: null, + _settingsPanelWrap: null, + _defaultPanel: null, + + get _strings() { + return Services.strings.createBundle( + "chrome://messenger/locale/preferences/applications.properties" + ); + }, + + init() { + this._list = document.getElementById("cloudFileView"); + this._buttonContainer = document.getElementById( + "addCloudFileAccountButtons" + ); + this._addAccountButton = document.getElementById("addCloudFileAccount"); + this._listContainer = document.getElementById( + "addCloudFileAccountListItems" + ); + this._removeAccountButton = document.getElementById( + "removeCloudFileAccount" + ); + this._defaultPanel = document.getElementById("cloudFileDefaultPanel"); + this._settingsPanelWrap = document.getElementById( + "cloudFileSettingsWrapper" + ); + + this.updateThreshold(); + this.rebuildView(); + + window.addEventListener("unload", this, { capture: false, once: true }); + + this._onAccountConfigured = this._onAccountConfigured.bind(this); + this._onProviderRegistered = this._onProviderRegistered.bind(this); + this._onProviderUnregistered = this._onProviderUnregistered.bind(this); + cloudFileAccounts.on("accountConfigured", this._onAccountConfigured); + cloudFileAccounts.on("providerRegistered", this._onProviderRegistered); + cloudFileAccounts.on("providerUnregistered", this._onProviderUnregistered); + + let element = document.getElementById("cloudFileThreshold"); + Preferences.addSyncFromPrefListener(element, () => this.readThreshold()); + Preferences.addSyncToPrefListener(element, () => this.writeThreshold()); + + this._initialized = true; + }, + + destroy() { + // Remove any controllers or observers here. + cloudFileAccounts.off("accountConfigured", this._onAccountConfigured); + cloudFileAccounts.off("providerRegistered", this._onProviderRegistered); + cloudFileAccounts.off("providerUnregistered", this._onProviderUnregistered); + }, + + _onAccountConfigured(event, account) { + for (let item of this._list.children) { + if (item.value == account.accountKey) { + item.querySelector(".configuredWarning").hidden = account.configured; + } + } + }, + + _onProviderRegistered(event, provider) { + let accounts = cloudFileAccounts.getAccountsForType(provider.type); + accounts.sort(this._sortDisplayNames); + + // Always add newly-enabled accounts to the end of the list, this makes + // it clearer to users what's happening. + for (let account of accounts) { + let item = this.makeRichListItemForAccount(account); + this._list.appendChild(item); + } + + this._buttonContainer.appendChild(this.makeButtonForProvider(provider)); + this._listContainer.appendChild(this.makeListItemForProvider(provider)); + }, + + _onProviderUnregistered(event, type) { + for (let item of [...this._list.children]) { + // If the provider is unregistered, getAccount returns null. + if (!cloudFileAccounts.getAccount(item.value)) { + if (item.hasAttribute("selected")) { + this._defaultPanel.hidden = false; + this._settingsPanelWrap.hidden = true; + if (this._settings) { + this._settings.remove(); + } + this._removeAccountButton.disabled = true; + } + item.remove(); + } + } + + for (let button of this._buttonContainer.children) { + if (button.getAttribute("value") == type) { + button.remove(); + } + } + + for (let item of this._listContainer.children) { + if (item.getAttribute("value") == type) { + item.remove(); + } + } + + if (this._buttonContainer.childElementCount < 1) { + this._buttonContainer.hidden = false; + this._addAccountButton.hidden = true; + } + }, + + makeRichListItemForAccount(aAccount) { + let rli = document.createXULElement("richlistitem"); + rli.setAttribute("align", "center"); + rli.classList.add("cloudfileAccount", "input-container"); + rli.setAttribute("value", aAccount.accountKey); + + let icon = document.createElement("img"); + icon.classList.add("typeIcon"); + if (aAccount.iconURL) { + icon.setAttribute("src", aAccount.iconURL); + } + icon.setAttribute("alt", ""); + rli.appendChild(icon); + + let label = document.createXULElement("label"); + label.setAttribute("crop", "end"); + label.setAttribute("flex", "1"); + label.setAttribute( + "value", + cloudFileAccounts.getDisplayName(aAccount.accountKey) + ); + label.addEventListener("click", this, true); + rli.appendChild(label); + + let input = document.createElement("input"); + input.setAttribute("type", "text"); + input.setAttribute("hidden", "hidden"); + input.addEventListener("blur", this); + input.addEventListener("keypress", this); + rli.appendChild(input); + + let warningIcon = document.createElement("img"); + warningIcon.setAttribute("class", "configuredWarning typeIcon"); + warningIcon.setAttribute("src", "chrome://global/skin/icons/warning.svg"); + // "title" provides the accessible name, not "alt". + warningIcon.setAttribute( + "title", + this._strings.GetStringFromName("notConfiguredYet") + ); + if (aAccount.configured) { + warningIcon.hidden = true; + } + rli.appendChild(warningIcon); + + return rli; + }, + + makeButtonForProvider(provider) { + let button = document.createXULElement("button"); + button.setAttribute("value", provider.type); + button.setAttribute( + "label", + this._strings.formatStringFromName("addProvider", [provider.displayName]) + ); + button.setAttribute( + "oncommand", + `gCloudFile.addCloudFileAccount("${provider.type}")` + ); + button.style.listStyleImage = `url("${provider.iconURL}")`; + return button; + }, + + makeListItemForProvider(provider) { + let menuitem = document.createXULElement("menuitem"); + menuitem.classList.add("menuitem-iconic"); + menuitem.setAttribute("value", provider.type); + menuitem.setAttribute("label", provider.displayName); + menuitem.setAttribute("image", provider.iconURL); + return menuitem; + }, + + // Sort the accounts by displayName. + _sortDisplayNames(a, b) { + let aName = a.displayName.toLowerCase(); + let bName = b.displayName.toLowerCase(); + return aName.localeCompare(bName); + }, + + rebuildView() { + // Clear the list of entries. + while (this._list.hasChildNodes()) { + this._list.lastChild.remove(); + } + + let accounts = cloudFileAccounts.accounts; + accounts.sort(this._sortDisplayNames); + + for (let account of accounts) { + let rli = this.makeRichListItemForAccount(account); + this._list.appendChild(rli); + } + + while (this._buttonContainer.hasChildNodes()) { + this._buttonContainer.lastChild.remove(); + } + + let providers = cloudFileAccounts.providers; + providers.sort(this._sortDisplayNames); + for (let provider of providers) { + this._buttonContainer.appendChild(this.makeButtonForProvider(provider)); + this._listContainer.appendChild(this.makeListItemForProvider(provider)); + } + }, + + onSelectionChanged(aEvent) { + if (!this._initialized || aEvent.target != this._list) { + return; + } + + // Get the selected item + let selection = this._list.selectedItem; + this._removeAccountButton.disabled = !selection; + if (!selection) { + this._defaultPanel.hidden = false; + this._settingsPanelWrap.hidden = true; + if (this._settings) { + this._settings.remove(); + } + return; + } + + this._showAccountInfo(selection.value); + }, + + _showAccountInfo(aAccountKey) { + let account = cloudFileAccounts.getAccount(aAccountKey); + this._defaultPanel.hidden = true; + this._settingsPanelWrap.hidden = false; + + let url = account.managementURL + `?accountId=${account.accountKey}`; + + let browser = document.createXULElement("browser"); + browser.setAttribute("type", "content"); + browser.setAttribute("remote", "true"); + browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE); + browser.setAttribute("forcemessagemanager", "true"); + if (account.extension) { + browser.setAttribute( + "initialBrowsingContextGroupId", + account.extension.policy.browsingContextGroupId + ); + } + browser.setAttribute("disableglobalhistory", "true"); + browser.setAttribute("messagemanagergroup", "webext-browsers"); + browser.setAttribute("autocompletepopup", "PopupAutoComplete"); + browser.setAttribute("selectmenulist", "ContentSelectDropdown"); + + browser.setAttribute("flex", "1"); + // Allows keeping dialog background color without hoops. + browser.setAttribute("transparent", "true"); + + // If we have a past browser, we replace it. Else append to the wrapper. + if (this._settings) { + this._settings.remove(); + } + + this._settingsPanelWrap.appendChild(browser); + this._settings = browser; + + ExtensionParent.apiManager.emit("extension-browser-inserted", browser); + browser.messageManager.loadFrameScript( + "chrome://extensions/content/ext-browser-content.js", + false, + true + ); + + let options = account.browserStyle + ? { stylesheets: ExtensionParent.extensionStylesheets } + : {}; + browser.messageManager.sendAsyncMessage("Extension:InitBrowser", options); + + browser.fixupAndLoadURIString(url, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + }, + + onListOverflow() { + if (this._buttonContainer.childElementCount > 1) { + this._buttonContainer.hidden = true; + this._addAccountButton.hidden = false; + } + }, + + addCloudFileAccount(aType) { + let account = cloudFileAccounts.createAccount(aType); + if (!account) { + return; + } + + let rli = this.makeRichListItemForAccount(account); + this._list.appendChild(rli); + this._list.selectItem(rli); + this._addAccountButton.removeAttribute("image"); + this._addAccountButton.setAttribute( + "label", + this._addAccountButton.getAttribute("defaultlabel") + ); + this._removeAccountButton.disabled = false; + }, + + removeCloudFileAccount() { + // Get the selected account key + let selection = this._list.selectedItem; + if (!selection) { + return; + } + + let accountKey = selection.value; + let accountName = cloudFileAccounts.getDisplayName(accountKey); + // Does the user really want to remove this account? + let confirmMessage = this._strings.formatStringFromName( + "dialog_removeAccount", + [accountName] + ); + + if (Services.prompt.confirm(null, "", confirmMessage)) { + this._list.clearSelection(); + cloudFileAccounts.removeAccount(accountKey); + let rli = this._list.querySelector( + "richlistitem[value='" + accountKey + "']" + ); + rli.remove(); + this._defaultPanel.hidden = false; + this._settingsPanelWrap.hidden = true; + if (this._settings) { + this._settings.remove(); + } + } + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "unload": + this.destroy(); + break; + case "click": { + let label = aEvent.target; + let item = label.parentNode; + let input = item.querySelector("input"); + if (!item.selected) { + return; + } + label.hidden = true; + input.value = label.value; + input.removeAttribute("hidden"); + input.focus(); + break; + } + case "blur": { + let input = aEvent.target; + let item = input.parentNode; + let label = item.querySelector("label"); + cloudFileAccounts.setDisplayName(item.value, input.value); + label.value = input.value; + label.hidden = false; + input.setAttribute("hidden", "hidden"); + break; + } + case "keypress": { + let input = aEvent.target; + let item = input.parentNode; + let label = item.querySelector("label"); + + if (aEvent.key == "Enter") { + cloudFileAccounts.setDisplayName(item.value, input.value); + label.value = input.value; + label.hidden = false; + input.setAttribute("hidden", "hidden"); + gCloudFile._list.focus(); + + aEvent.preventDefault(); + } else if (aEvent.key == "Escape") { + input.value = label.value; + label.hidden = false; + input.setAttribute("hidden", "hidden"); + gCloudFile._list.focus(); + + aEvent.preventDefault(); + } + } + } + }, + + readThreshold() { + let pref = Preferences.get("mail.compose.big_attachments.threshold_kb"); + return pref.value / 1024; + }, + + writeThreshold() { + let threshold = document.getElementById("cloudFileThreshold"); + let intValue = parseInt(threshold.value, 10); + return isNaN(intValue) ? 0 : intValue * 1024; + }, + + updateThreshold() { + document.getElementById("cloudFileThreshold").disabled = !Preferences.get( + "mail.compose.big_attachments.notify" + ).value; + }, +}; + +Preferences.get("mail.compose.autosave").on( + "change", + gComposePane.updateAutosave +); +Preferences.get("mail.compose.attachment_reminder").on( + "change", + gComposePane.updateAttachmentCheck +); +Preferences.get("msgcompose.default_colors").on( + "change", + gComposePane.updateUseReaderDefaults +); +Preferences.get("ldap_2.autoComplete.useDirectory").on( + "change", + gComposePane.enableAutocomplete +); +Preferences.get("mail.collect_email_address_outgoing").on( + "change", + gComposePane.updateEmailCollection +); +Preferences.get("mail.compose.big_attachments.notify").on( + "change", + gCloudFile.updateThreshold +); diff --git a/comm/mail/components/preferences/connection.js b/comm/mail/components/preferences/connection.js new file mode 100644 index 0000000000..686c2950cf --- /dev/null +++ b/comm/mail/components/preferences/connection.js @@ -0,0 +1,597 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* 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 ../../../../toolkit/content/preferencesBindings.js */ +/* import-globals-from ./extensionControlled.js */ + +Preferences.addAll([ + // Add network.proxy.autoconfig_url before network.proxy.type so they're + // both initialized when network.proxy.type initialization triggers a call to + // gConnectionsDialog.updateReloadButton(). + { id: "network.proxy.autoconfig_url", type: "string" }, + { id: "network.proxy.type", type: "int" }, + { id: "network.proxy.http", type: "string" }, + { id: "network.proxy.http_port", type: "int" }, + { id: "network.proxy.ssl", type: "string" }, + { id: "network.proxy.ssl_port", type: "int" }, + { id: "network.proxy.socks", type: "string" }, + { id: "network.proxy.socks_port", type: "int" }, + { id: "network.proxy.socks_version", type: "int" }, + { id: "network.proxy.socks_remote_dns", type: "bool" }, + { id: "network.proxy.no_proxies_on", type: "string" }, + { id: "network.proxy.share_proxy_settings", type: "bool" }, + { id: "signon.autologin.proxy", type: "bool" }, + { id: "pref.advanced.proxies.disable_button.reload", type: "bool" }, + { id: "network.proxy.backup.ssl", type: "string" }, + { id: "network.proxy.backup.ssl_port", type: "int" }, + { id: "network.trr.mode", type: "int" }, + { id: "network.trr.uri", type: "string" }, + { id: "network.trr.resolvers", type: "string" }, + { id: "network.trr.custom_uri", type: "string" }, +]); + +window.addEventListener( + "DOMContentLoaded", + () => { + Preferences.get("network.proxy.type").on( + "change", + gConnectionsDialog.proxyTypeChanged.bind(gConnectionsDialog) + ); + Preferences.get("network.proxy.socks_version").on( + "change", + gConnectionsDialog.updateDNSPref.bind(gConnectionsDialog) + ); + + Preferences.get("network.trr.uri").on("change", () => { + gConnectionsDialog.updateDnsOverHttpsUI(); + }); + + Preferences.get("network.trr.resolvers").on("change", () => { + gConnectionsDialog.initDnsOverHttpsUI(); + }); + + Preferences.addSyncFromPrefListener( + document.getElementById("networkProxyType"), + () => gConnectionsDialog.readProxyType() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("networkProxyHTTP"), + () => gConnectionsDialog.readHTTPProxyServer() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("networkProxyHTTP_Port"), + () => gConnectionsDialog.readHTTPProxyPort() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("shareAllProxies"), + () => gConnectionsDialog.updateProtocolPrefs() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("networkProxySSL"), + () => gConnectionsDialog.readProxyProtocolPref("ssl", false) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("networkProxySSL_Port"), + () => gConnectionsDialog.readProxyProtocolPref("ssl", true) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("networkProxySOCKS"), + () => gConnectionsDialog.readProxyProtocolPref("socks", false) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("networkProxySOCKS_Port"), + () => gConnectionsDialog.readProxyProtocolPref("socks", true) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("networkProxySOCKSVersion"), + () => gConnectionsDialog.updateDNSPref() + ); + + // XXX: We can't init the DNS-over-HTTPs UI until the syncfrompref for network.trr.mode + // has been called. The uiReady promise will be resolved after the first call to + // readDnsOverHttpsMode and the subsequent call to initDnsOverHttpsUI has happened. + gConnectionsDialog.uiReady = new Promise(resolve => { + gConnectionsDialog._areTrrPrefsReady = false; + gConnectionsDialog._handleTrrPrefsReady = resolve; + }).then(() => { + gConnectionsDialog.initDnsOverHttpsUI(); + }); + + let element = document.getElementById("networkDnsOverHttps"); + Preferences.addSyncFromPrefListener(element, () => + gConnectionsDialog.readDnsOverHttpsMode() + ); + Preferences.addSyncToPrefListener(element, () => + gConnectionsDialog.writeDnsOverHttpsMode() + ); + document.documentElement.addEventListener("beforeaccept", e => + gConnectionsDialog.beforeAccept(e) + ); + + document + .getElementById("proxyExtensionDisable") + .addEventListener("click", disableControllingProxyExtension); + gConnectionsDialog.updateProxySettingsUI(); + initializeProxyUI(gConnectionsDialog); + }, + { once: true, capture: true } +); + +var gConnectionsDialog = { + beforeAccept(event) { + let dnsOverHttpsResolverChoice = document.getElementById( + "networkDnsOverHttpsResolverChoices" + ).value; + if (dnsOverHttpsResolverChoice == "custom") { + let customValue = document + .getElementById("networkCustomDnsOverHttpsInput") + .value.trim(); + if (customValue) { + Services.prefs.setStringPref("network.trr.uri", customValue); + } else { + Services.prefs.clearUserPref("network.trr.uri"); + } + } else { + Services.prefs.setStringPref( + "network.trr.uri", + dnsOverHttpsResolverChoice + ); + } + + var proxyTypePref = Preferences.get("network.proxy.type"); + if (proxyTypePref.value == 2) { + this.doAutoconfigURLFixup(); + return; + } + + if (proxyTypePref.value != 1) { + return; + } + + var httpProxyURLPref = Preferences.get("network.proxy.http"); + var httpProxyPortPref = Preferences.get("network.proxy.http_port"); + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + + // If the port is 0 and the proxy server is specified, focus on the port and cancel submission. + for (let prefName of ["http", "ssl", "socks"]) { + let proxyPortPref = Preferences.get( + "network.proxy." + prefName + "_port" + ); + let proxyPref = Preferences.get("network.proxy." + prefName); + // Only worry about ports which are currently active. If the share option is on, then ignore + // all ports except the HTTP and SOCKS port + if ( + proxyPref.value != "" && + proxyPortPref.value == 0 && + (prefName == "http" || prefName == "socks" || !shareProxiesPref.value) + ) { + document + .getElementById("networkProxy" + prefName.toUpperCase() + "_Port") + .focus(); + event.preventDefault(); + return; + } + } + + // In the case of a shared proxy preference, backup the current values and update with the HTTP value + if (shareProxiesPref.value) { + var proxyServerURLPref = Preferences.get("network.proxy.ssl"); + var proxyPortPref = Preferences.get("network.proxy.ssl_port"); + var backupServerURLPref = Preferences.get("network.proxy.backup.ssl"); + var backupPortPref = Preferences.get("network.proxy.backup.ssl_port"); + backupServerURLPref.value = + backupServerURLPref.value || proxyServerURLPref.value; + backupPortPref.value = backupPortPref.value || proxyPortPref.value; + proxyServerURLPref.value = httpProxyURLPref.value; + proxyPortPref.value = httpProxyPortPref.value; + } + + this.sanitizeNoProxiesPref(); + }, + + checkForSystemProxy() { + if ("@mozilla.org/system-proxy-settings;1" in Cc) { + document.getElementById("systemPref").removeAttribute("hidden"); + } + }, + + proxyTypeChanged() { + var proxyTypePref = Preferences.get("network.proxy.type"); + + // Update http + var httpProxyURLPref = Preferences.get("network.proxy.http"); + httpProxyURLPref.updateControlDisabledState(proxyTypePref.value != 1); + var httpProxyPortPref = Preferences.get("network.proxy.http_port"); + httpProxyPortPref.updateControlDisabledState(proxyTypePref.value != 1); + + // Now update the other protocols + this.updateProtocolPrefs(); + + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + shareProxiesPref.updateControlDisabledState(proxyTypePref.value != 1); + var autologinProxyPref = Preferences.get("signon.autologin.proxy"); + autologinProxyPref.updateControlDisabledState(proxyTypePref.value == 0); + var noProxiesPref = Preferences.get("network.proxy.no_proxies_on"); + noProxiesPref.updateControlDisabledState(proxyTypePref.value == 0); + + var autoconfigURLPref = Preferences.get("network.proxy.autoconfig_url"); + autoconfigURLPref.updateControlDisabledState(proxyTypePref.value != 2); + + this.updateReloadButton(); + + document.getElementById("networkProxyNoneLocalhost").hidden = + Services.prefs.getBoolPref( + "network.proxy.allow_hijacking_localhost", + false + ); + }, + + updateDNSPref() { + var socksVersionPref = Preferences.get("network.proxy.socks_version"); + var socksDNSPref = Preferences.get("network.proxy.socks_remote_dns"); + var proxyTypePref = Preferences.get("network.proxy.type"); + var isDefinitelySocks4 = + proxyTypePref.value == 1 && socksVersionPref.value == 4; + socksDNSPref.updateControlDisabledState( + isDefinitelySocks4 || proxyTypePref.value == 0 + ); + return undefined; + }, + + updateReloadButton() { + // Disable the "Reload PAC" button if the selected proxy type is not PAC or + // if the current value of the PAC textbox does not match the value stored + // in prefs. Likewise, disable the reload button if PAC is not configured + // in prefs. + + var typedURL = document.getElementById("networkProxyAutoconfigURL").value; + var proxyTypeCur = Preferences.get("network.proxy.type").value; + + var pacURL = Services.prefs.getCharPref("network.proxy.autoconfig_url"); + var proxyType = Services.prefs.getIntPref("network.proxy.type"); + + var disableReloadPref = Preferences.get( + "pref.advanced.proxies.disable_button.reload" + ); + disableReloadPref.updateControlDisabledState( + proxyTypeCur != 2 || proxyType != 2 || typedURL != pacURL + ); + }, + + readProxyType() { + this.proxyTypeChanged(); + return undefined; + }, + + updateProtocolPrefs() { + var proxyTypePref = Preferences.get("network.proxy.type"); + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + var proxyPrefs = ["ssl", "socks"]; + for (var i = 0; i < proxyPrefs.length; ++i) { + var proxyServerURLPref = Preferences.get( + "network.proxy." + proxyPrefs[i] + ); + var proxyPortPref = Preferences.get( + "network.proxy." + proxyPrefs[i] + "_port" + ); + + // Restore previous per-proxy custom settings, if present. + if (proxyPrefs[i] != "socks" && !shareProxiesPref.value) { + var backupServerURLPref = Preferences.get( + "network.proxy.backup." + proxyPrefs[i] + ); + var backupPortPref = Preferences.get( + "network.proxy.backup." + proxyPrefs[i] + "_port" + ); + if (backupServerURLPref.hasUserValue) { + proxyServerURLPref.value = backupServerURLPref.value; + backupServerURLPref.reset(); + } + if (backupPortPref.hasUserValue) { + proxyPortPref.value = backupPortPref.value; + backupPortPref.reset(); + } + } + + proxyServerURLPref.updateElements(); + proxyPortPref.updateElements(); + let prefIsShared = proxyPrefs[i] != "socks" && shareProxiesPref.value; + proxyServerURLPref.updateControlDisabledState( + proxyTypePref.value != 1 || prefIsShared + ); + proxyPortPref.updateControlDisabledState( + proxyTypePref.value != 1 || prefIsShared + ); + } + var socksVersionPref = Preferences.get("network.proxy.socks_version"); + socksVersionPref.updateControlDisabledState(proxyTypePref.value != 1); + this.updateDNSPref(); + return undefined; + }, + + readProxyProtocolPref(aProtocol, aIsPort) { + if (aProtocol != "socks") { + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + if (shareProxiesPref.value) { + var pref = Preferences.get( + "network.proxy.http" + (aIsPort ? "_port" : "") + ); + return pref.value; + } + + var backupPref = Preferences.get( + "network.proxy.backup." + aProtocol + (aIsPort ? "_port" : "") + ); + return backupPref.hasUserValue ? backupPref.value : undefined; + } + return undefined; + }, + + reloadPAC() { + Cc["@mozilla.org/network/protocol-proxy-service;1"] + .getService() + .reloadPAC(); + }, + + doAutoconfigURLFixup() { + var autoURL = document.getElementById("networkProxyAutoconfigURL"); + var autoURLPref = Preferences.get("network.proxy.autoconfig_url"); + try { + autoURLPref.value = autoURL.value = Services.uriFixup.getFixupURIInfo( + autoURL.value, + 0 + ).preferredURI.spec; + } catch (ex) {} + }, + + sanitizeNoProxiesPref() { + var noProxiesPref = Preferences.get("network.proxy.no_proxies_on"); + // replace substrings of ; and \n with commas if they're neither immediately + // preceded nor followed by a valid separator character + noProxiesPref.value = noProxiesPref.value.replace( + /([^, \n;])[;\n]+(?![,\n;])/g, + "$1," + ); + // replace any remaining ; and \n since some may follow commas, etc. + noProxiesPref.value = noProxiesPref.value.replace(/[;\n]/g, ""); + }, + + readHTTPProxyServer() { + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + if (shareProxiesPref.value) { + this.updateProtocolPrefs(); + } + return undefined; + }, + + readHTTPProxyPort() { + var shareProxiesPref = Preferences.get( + "network.proxy.share_proxy_settings" + ); + if (shareProxiesPref.value) { + this.updateProtocolPrefs(); + } + return undefined; + }, + + getProxyControls() { + let controlGroup = document.getElementById("networkProxyType"); + return [ + ...controlGroup.querySelectorAll(":scope > radio"), + ...controlGroup.querySelectorAll("label"), + ...controlGroup.querySelectorAll("input"), + ...controlGroup.querySelectorAll("checkbox"), + ...document.querySelectorAll("#networkProxySOCKSVersion > radio"), + ...document.querySelectorAll("#ConnectionsDialogPane > checkbox"), + ]; + }, + + // Update the UI to show/hide the extension controlled message for + // proxy settings. + async updateProxySettingsUI() { + let isLocked = API_PROXY_PREFS.some(pref => + Services.prefs.prefIsLocked(pref) + ); + + function setInputsDisabledState(isControlled) { + for (let element of gConnectionsDialog.getProxyControls()) { + element.disabled = isControlled; + } + gConnectionsDialog.proxyTypeChanged(); + } + + if (isLocked) { + // An extension can't control this setting if any pref is locked. + hideControllingProxyExtension(); + } else { + handleControllingProxyExtension().then(setInputsDisabledState); + } + }, + + get dnsOverHttpsResolvers() { + let rawValue = Preferences.get("network.trr.resolvers", "").value; + // if there's no default, we'll hold its position with an empty string + let defaultURI = Preferences.get("network.trr.uri", "").defaultValue; + let providers = []; + if (rawValue) { + try { + providers = JSON.parse(rawValue); + } catch (ex) { + console.error( + `Bad JSON data in pref network.trr.resolvers: ${rawValue}` + ); + } + } + if (!Array.isArray(providers)) { + console.error( + `Expected a JSON array in network.trr.resolvers: ${rawValue}` + ); + providers = []; + } + let defaultIndex = providers.findIndex(p => p.url == defaultURI); + if (defaultIndex == -1 && defaultURI) { + // the default value for the pref isn't included in the resolvers list + // so we'll make a stub for it. Without an id, we'll have to use the url as the label + providers.unshift({ url: defaultURI }); + } + return providers; + }, + + isDnsOverHttpsLocked() { + return Services.prefs.prefIsLocked("network.trr.mode"); + }, + + isDnsOverHttpsEnabled() { + // values outside 1:4 are considered falsey/disabled in this context + let trrPref = Preferences.get("network.trr.mode"); + let enabled = trrPref.value > 0 && trrPref.value < 5; + return enabled; + }, + + readDnsOverHttpsMode() { + // called to update checked element property to reflect current pref value + let enabled = this.isDnsOverHttpsEnabled(); + let uriPref = Preferences.get("network.trr.uri"); + uriPref.updateControlDisabledState(!enabled || this.isDnsOverHttpsLocked()); + // this is the first signal we get when the prefs are available, so + // lazy-init if appropriate + if (!this._areTrrPrefsReady) { + this._areTrrPrefsReady = true; + this._handleTrrPrefsReady(); + } else { + this.updateDnsOverHttpsUI(); + } + return enabled; + }, + + writeDnsOverHttpsMode() { + // called to update pref with user change + let trrModeCheckbox = document.getElementById("networkDnsOverHttps"); + // we treat checked/enabled as mode 2 + return trrModeCheckbox.checked ? 2 : 0; + }, + + updateDnsOverHttpsUI() { + // init and update of the UI must wait until the pref values are ready + if (!this._areTrrPrefsReady) { + return; + } + let [menu, customInput] = this.getDnsOverHttpsControls(); + let customContainer = document.getElementById( + "customDnsOverHttpsContainer" + ); + let customURI = Preferences.get("network.trr.custom_uri").value; + let currentURI = Preferences.get("network.trr.uri").value; + let resolvers = this.dnsOverHttpsResolvers; + let isCustom = menu.value == "custom"; + + if (this.isDnsOverHttpsEnabled()) { + this.toggleDnsOverHttpsUI(false); + if (isCustom) { + // if the current and custom_uri values mismatch, update the uri pref + if ( + currentURI && + !customURI && + !resolvers.find(r => r.url == currentURI) + ) { + Services.prefs.setStringPref("network.trr.custom_uri", currentURI); + } + } + } else { + this.toggleDnsOverHttpsUI(true); + } + + if (!menu.disabled && isCustom) { + customContainer.hidden = false; + customInput.disabled = false; + } else { + customContainer.hidden = true; + customInput.disabled = true; + } + + // The height has likely changed, find our SubDialog and tell it to resize. + requestAnimationFrame(() => { + let dialogs = window.opener.gSubDialog._dialogs; + let dialog = dialogs.find(d => d._frame.contentDocument == document); + if (dialog) { + dialog.resizeVertically(); + } + }); + }, + + getDnsOverHttpsControls() { + return [ + document.getElementById("networkDnsOverHttpsResolverChoices"), + document.getElementById("networkCustomDnsOverHttpsInput"), + document.getElementById("networkDnsOverHttpsResolverChoicesLabel"), + document.getElementById("networkCustomDnsOverHttpsInputLabel"), + ]; + }, + + toggleDnsOverHttpsUI(disabled) { + for (let element of this.getDnsOverHttpsControls()) { + element.disabled = disabled; + } + }, + + initDnsOverHttpsUI() { + let resolvers = this.dnsOverHttpsResolvers; + let defaultURI = Preferences.get("network.trr.uri").defaultValue; + let currentURI = Preferences.get("network.trr.uri").value; + let menu = document.getElementById("networkDnsOverHttpsResolverChoices"); + + // populate the DNS-Over-HTTPs resolver list + menu.removeAllItems(); + for (let resolver of resolvers) { + let item = menu.appendItem(undefined, resolver.url); + if (resolver.url == defaultURI) { + document.l10n.setAttributes( + item, + "connection-dns-over-https-url-item-default", + { + name: resolver.name || resolver.url, + } + ); + } else { + item.label = resolver.name || resolver.url; + } + } + let lastItem = menu.appendItem(undefined, "custom"); + document.l10n.setAttributes( + lastItem, + "connection-dns-over-https-url-custom" + ); + + // set initial selection in the resolver provider picker + let selectedIndex = currentURI + ? resolvers.findIndex(r => r.url == currentURI) + : 0; + if (selectedIndex == -1) { + // select the last "Custom" item + selectedIndex = menu.itemCount - 1; + } + menu.selectedIndex = selectedIndex; + + if (this.isDnsOverHttpsLocked()) { + // disable all the options and the checkbox itself to disallow enabling them + this.toggleDnsOverHttpsUI(true); + document.getElementById("networkDnsOverHttps").disabled = true; + } else { + this.toggleDnsOverHttpsUI(false); + this.updateDnsOverHttpsUI(); + document.getElementById("networkDnsOverHttps").disabled = false; + } + }, +}; diff --git a/comm/mail/components/preferences/connection.xhtml b/comm/mail/components/preferences/connection.xhtml new file mode 100644 index 0000000000..1bbb822f66 --- /dev/null +++ b/comm/mail/components/preferences/connection.xhtml @@ -0,0 +1,264 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE window> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="connection-dialog-window2" + style="min-width: 49em" + onload="gConnectionsDialog.checkForSystemProxy();" +> + <dialog id="ConnectionsDialog" dlgbuttons="accept,cancel"> + <linkset> + <html:link + rel="localization" + href="messenger/preferences/connection.ftl" + /> + <html:link + rel="localization" + href="messenger/preferences/preferences.ftl" + /> + <html:link rel="localization" href="branding/brand.ftl" /> + </linkset> + + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://messenger/content/preferences/extensionControlled.js" /> + <script src="chrome://messenger/content/preferences/connection.js" /> + + <!-- Need a wrapper div within the xul:dialog, which otherwise does not give + - enough height for the flex display. + - REMOVE when we use HTML only. --> + <html:div> + <html:div id="proxyExtensionContent" hidden="hidden"> + <html:p id="proxyExtensionDescription"> + <html:img data-l10n-name="extension-icon" /> + </html:p> + <html:button + id="proxyExtensionDisable" + data-l10n-id="disable-extension-button" + > + </html:button> + </html:div> + </html:div> + + <html:div> + <html:fieldset> + <html:legend data-l10n-id="connection-proxy-legend"></html:legend> + + <radiogroup id="networkProxyType" preference="network.proxy.type"> + <radio value="0" data-l10n-id="proxy-type-no" /> + <radio value="4" data-l10n-id="proxy-type-wpad" /> + <radio + value="5" + data-l10n-id="proxy-type-system" + id="systemPref" + hidden="true" + /> + <radio value="1" data-l10n-id="proxy-type-manual" /> + <box id="proxy-grid" class="indent" flex="1"> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label + data-l10n-id="proxy-http-label" + control="networkProxyHTTP" + /> + </hbox> + <hbox align="center" class="input-container"> + <html:input + id="networkProxyHTTP" + type="text" + preference="network.proxy.http" + /> + <label + data-l10n-id="http-port-label" + control="networkProxyHTTP_Port" + /> + <html:input + id="networkProxyHTTP_Port" + type="number" + class="size5" + max="65535" + preference="network.proxy.http_port" + /> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <hbox /> + <hbox> + <checkbox + id="shareAllProxies" + data-l10n-id="proxy-http-sharing" + preference="network.proxy.share_proxy_settings" + class="align-no-label" + /> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label + data-l10n-id="proxy-https-label" + control="networkProxySSL" + /> + </hbox> + <hbox align="center" class="input-container"> + <html:input + id="networkProxySSL" + type="text" + preference="network.proxy.ssl" + /> + <label + data-l10n-id="ssl-port-label" + control="networkProxySSL_Port" + /> + <html:input + id="networkProxySSL_Port" + type="number" + class="size5" + max="65535" + preference="network.proxy.ssl_port" + /> + </hbox> + </html:div> + <separator class="thin" /> + <html:div class="proxy-grid-row"> + <hbox pack="end"> + <label + data-l10n-id="proxy-socks-label" + control="networkProxySOCKS" + /> + </hbox> + <hbox align="center" class="input-container"> + <html:input + id="networkProxySOCKS" + type="text" + preference="network.proxy.socks" + /> + <label + data-l10n-id="socks-port-label" + control="networkProxySOCKS_Port" + /> + <html:input + id="networkProxySOCKS_Port" + type="number" + class="size5" + max="65535" + preference="network.proxy.socks_port" + /> + </hbox> + </html:div> + <html:div class="proxy-grid-row"> + <spacer /> + <radiogroup + id="networkProxySOCKSVersion" + orient="horizontal" + class="align-no-label" + preference="network.proxy.socks_version" + > + <radio + id="networkProxySOCKSVersion4" + value="4" + data-l10n-id="proxy-socks4-label" + /> + <radio + id="networkProxySOCKSVersion5" + value="5" + data-l10n-id="proxy-socks5-label" + /> + </radiogroup> + </html:div> + </box> + <radio value="2" data-l10n-id="proxy-type-auto" /> + <hbox class="indent input-container" flex="1" align="center"> + <html:input + id="networkProxyAutoconfigURL" + type="url" + preference="network.proxy.autoconfig_url" + oninput="gConnectionsDialog.updateReloadButton();" + /> + <button + id="autoReload" + data-l10n-id="proxy-reload-label" + oncommand="gConnectionsDialog.reloadPAC();" + preference="pref.advanced.proxies.disable_button.reload" + /> + </hbox> + </radiogroup> + </html:fieldset> + </html:div> + <separator class="thin" /> + <label data-l10n-id="no-proxy-label" control="networkProxyNone" /> + <html:textarea + id="networkProxyNone" + rows="2" + preference="network.proxy.no_proxies_on" + /> + <label data-l10n-id="no-proxy-example" control="networkProxyNone" /> + <label + id="networkProxyNoneLocalhost" + control="networkProxyNone" + data-l10n-id="connection-proxy-noproxy-localhost-desc-2" + /> + <separator class="thin" /> + <checkbox + id="autologinProxy" + data-l10n-id="proxy-password-prompt" + preference="signon.autologin.proxy" + /> + <checkbox + id="networkProxySOCKSRemoteDNS" + preference="network.proxy.socks_remote_dns" + data-l10n-id="proxy-remote-dns" + /> + <separator class="thin" /> + <checkbox + id="networkDnsOverHttps" + data-l10n-id="proxy-enable-doh" + preference="network.trr.mode" + /> + <box id="dnsOverHttps-grid" class="indent" flex="1"> + <html:div class="dnsOverHttps-grid-row"> + <hbox pack="end"> + <label + id="networkDnsOverHttpsResolverChoicesLabel" + data-l10n-id="connection-dns-over-https-url-resolver" + control="networkDnsOverHttpsResolverChoices" + /> + </hbox> + <menulist + id="networkDnsOverHttpsResolverChoices" + flex="1" + oncommand="gConnectionsDialog.updateDnsOverHttpsUI()" + /> + </html:div> + <html:div + class="dnsOverHttps-grid-row" + id="customDnsOverHttpsContainer" + hidden="hidden" + > + <hbox> + <label + id="networkCustomDnsOverHttpsInputLabel" + data-l10n-id="connection-dns-over-https-custom-label" + control="networkCustomDnsOverHttpsInput" + /> + </hbox> + <html:input + id="networkCustomDnsOverHttpsInput" + type="url" + style="flex: 1" + preference="network.trr.custom_uri" + /> + </html:div> + </box> + </dialog> +</window> diff --git a/comm/mail/components/preferences/cookies.js b/comm/mail/components/preferences/cookies.js new file mode 100644 index 0000000000..da06eb7e5a --- /dev/null +++ b/comm/mail/components/preferences/cookies.js @@ -0,0 +1,993 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +var { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); +ChromeUtils.defineESModuleGetters(this, { + ContextualIdentityService: + "resource://gre/modules/ContextualIdentityService.sys.mjs", +}); + +var gCookiesWindow = { + _hosts: {}, + _hostOrder: [], + _tree: null, + _bundle: null, + + init() { + Services.obs.addObserver(this, "cookie-changed"); + Services.obs.addObserver(this, "perm-changed"); + + this._bundle = document.getElementById("bundlePreferences"); + this._tree = document.getElementById("cookiesList"); + + this._populateList(true); + + document.getElementById("filter").focus(); + + if (!Services.prefs.getBoolPref("privacy.userContext.enabled")) { + document.getElementById("userContext").hidden = true; + document.getElementById("userContextLabel").hidden = true; + } + }, + + uninit() { + Services.obs.removeObserver(this, "cookie-changed"); + Services.obs.removeObserver(this, "perm-changed"); + }, + + _populateList(aInitialLoad) { + this._loadCookies(); + this._tree.view = this._view; + if (aInitialLoad) { + this.sort("rawHost"); + } + if (this._view.rowCount > 0) { + this._tree.view.selection.select(0); + } + + if (aInitialLoad) { + if ( + "arguments" in window && + window.arguments[2] && + window.arguments[2].filterString + ) { + this.setFilter(window.arguments[2].filterString); + } + } else if (document.getElementById("filter").value != "") { + this.filter(); + } + + this._saveState(); + }, + + _cookieEquals(aCookieA, aCookieB, aStrippedHost) { + return ( + aCookieA.rawHost == aStrippedHost && + aCookieA.name == aCookieB.name && + aCookieA.path == aCookieB.path && + ChromeUtils.isOriginAttributesEqual( + aCookieA.originAttributes, + aCookieB.originAttributes + ) + ); + }, + + observe(aCookie, aTopic, aData) { + if (aTopic != "cookie-changed") { + return; + } + + if (aCookie instanceof Ci.nsICookie) { + var strippedHost = this._makeStrippedHost(aCookie.host); + if (aData == "changed") { + this._handleCookieChanged(aCookie, strippedHost); + } else if (aData == "added") { + this._handleCookieAdded(aCookie, strippedHost); + } + } else if (aData == "cleared") { + this._hosts = {}; + this._hostOrder = []; + + var oldRowCount = this._view._rowCount; + this._view._rowCount = 0; + this._tree.rowCountChanged(0, -oldRowCount); + this._view.selection.clearSelection(); + } else if (aData == "reload") { + // first, clear any existing entries + this.observe(aCookie, aTopic, "cleared"); + + // then, reload the list + this._populateList(false); + } + + // We don't yet handle aData == "deleted" - it's a less common case + // and is rather complicated as selection tracking is difficult + }, + + _handleCookieChanged(changedCookie, strippedHost) { + var rowIndex = 0; + var cookieItem = null; + if (!this._view._filtered) { + for (var i = 0; i < this._hostOrder.length; ++i) { + // (var host in this._hosts) { + ++rowIndex; + var hostItem = this._hosts[this._hostOrder[i]]; // var hostItem = this._hosts[host]; + if (this._hostOrder[i] == strippedHost) { + // host == strippedHost) { + // Host matches, look for the cookie within this Host collection + // and update its data + for (var j = 0; j < hostItem.cookies.length; ++j) { + ++rowIndex; + var currCookie = hostItem.cookies[j]; + if (this._cookieEquals(currCookie, changedCookie, strippedHost)) { + currCookie.value = changedCookie.value; + currCookie.isSecure = changedCookie.isSecure; + currCookie.isDomain = changedCookie.isDomain; + currCookie.expires = changedCookie.expires; + cookieItem = currCookie; + break; + } + } + } else if (hostItem.open) { + rowIndex += hostItem.cookies.length; + } + } + } else { + // Just walk the filter list to find the item. It doesn't matter that + // we don't update the main Host collection when we do this, because + // when the filter is reset the Host collection is rebuilt anyway. + for (rowIndex = 0; rowIndex < this._view._filterSet.length; ++rowIndex) { + currCookie = this._view._filterSet[rowIndex]; + if (this._cookieEquals(currCookie, changedCookie, strippedHost)) { + currCookie.value = changedCookie.value; + currCookie.isSecure = changedCookie.isSecure; + currCookie.isDomain = changedCookie.isDomain; + currCookie.expires = changedCookie.expires; + cookieItem = currCookie; + break; + } + } + } + + // Make sure the tree display is up to date... + this._tree.invalidateRow(rowIndex); + // ... and if the cookie is selected, update the displayed metadata too + if (cookieItem != null && this._view.selection.currentIndex == rowIndex) { + this._updateCookieData(cookieItem); + } + }, + + _handleCookieAdded(changedCookie, strippedHost) { + var rowCountImpact = 0; + var addedHost = { value: 0 }; + this._addCookie(strippedHost, changedCookie, addedHost); + if (!this._view._filtered) { + // The Host collection for this cookie already exists, and it's not open, + // so don't increment the rowCountImpact because the user is not going to + // see the additional rows as they're hidden. + if (addedHost.value || this._hosts[strippedHost].open) { + ++rowCountImpact; + } + } else { + // We're in search mode, and the cookie being added matches + // the search condition, so add it to the list. + var c = this._makeCookieObject(strippedHost, changedCookie); + if (this._cookieMatchesFilter(c)) { + this._view._filterSet.push( + this._makeCookieObject(strippedHost, changedCookie) + ); + ++rowCountImpact; + } + } + // Now update the tree display at the end (we could/should re run the sort + // if any to get the position correct.) + var oldRowCount = this._rowCount; + this._view._rowCount += rowCountImpact; + this._tree.rowCountChanged(oldRowCount - 1, rowCountImpact); + + document.getElementById("removeAllCookies").disabled = this._view._filtered; + }, + + _view: { + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), + _filtered: false, + _filterSet: [], + _filterValue: "", + _rowCount: 0, + _cacheValid: 0, + _cacheItems: [], + get rowCount() { + return this._rowCount; + }, + + _getItemAtIndex(aIndex) { + if (this._filtered) { + return this._filterSet[aIndex]; + } + + var start = 0; + var count = 0, + hostIndex = 0; + + var cacheIndex = Math.min(this._cacheValid, aIndex); + if (cacheIndex > 0) { + var cacheItem = this._cacheItems[cacheIndex]; + start = cacheItem.start; + count = hostIndex = cacheItem.count; + } + + for (let i = start; i < gCookiesWindow._hostOrder.length; ++i) { + let currHost = gCookiesWindow._hosts[gCookiesWindow._hostOrder[i]]; + if (!currHost) { + continue; + } + if (count == aIndex) { + return currHost; + } + hostIndex = count; + + let cacheEntry = { start: i, count }; + var cacheStart = count; + + if (currHost.open) { + if (count < aIndex && aIndex <= count + currHost.cookies.length) { + // We are looking for an entry within this host's children, + // enumerate them looking for the index. + ++count; + for (let j = 0; j < currHost.cookies.length; ++j) { + if (count == aIndex) { + let cookie = currHost.cookies[j]; + cookie.parentIndex = hostIndex; + return cookie; + } + ++count; + } + } else { + // A host entry was open, but we weren't looking for an index + // within that host entry's children, so skip forward over the + // entry's children. We need to add one to increment for the + // host value too. + count += currHost.cookies.length + 1; + } + } else { + ++count; + } + + for (let j = cacheStart; j < count; j++) { + this._cacheItems[j] = cacheEntry; + } + this._cacheValid = count - 1; + } + return null; + }, + + _removeItemAtIndex(aIndex, aCount) { + var removeCount = aCount === undefined ? 1 : aCount; + if (this._filtered) { + // remove the cookies from the unfiltered set so that they + // don't reappear when the filter is changed. See bug 410863. + for (let i = aIndex; i < aIndex + removeCount; ++i) { + let item = this._filterSet[i]; + let parent = gCookiesWindow._hosts[item.rawHost]; + for (var j = 0; j < parent.cookies.length; ++j) { + if (item == parent.cookies[j]) { + parent.cookies.splice(j, 1); + break; + } + } + } + this._filterSet.splice(aIndex, removeCount); + return; + } + + let item = this._getItemAtIndex(aIndex); + if (!item) { + return; + } + this._invalidateCache(aIndex - 1); + if (item.container) { + gCookiesWindow._hosts[item.rawHost] = null; + } else { + let parent = this._getItemAtIndex(item.parentIndex); + for (let i = 0; i < parent.cookies.length; ++i) { + var cookie = parent.cookies[i]; + if ( + item.rawHost == cookie.rawHost && + item.name == cookie.name && + item.path == cookie.path && + ChromeUtils.isOriginAttributesEqual( + item.originAttributes, + cookie.originAttributes + ) + ) { + parent.cookies.splice(i, removeCount); + } + } + } + }, + + _invalidateCache(aIndex) { + this._cacheValid = Math.min(this._cacheValid, aIndex); + }, + + getCellText(aIndex, aColumn) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) { + return ""; + } + if (aColumn.id == "domainCol") { + return item.rawHost; + } + if (aColumn.id == "nameCol") { + return "name" in item ? item.name : ""; + } + } else if (aColumn.id == "domainCol") { + return this._filterSet[aIndex].rawHost; + } else if (aColumn.id == "nameCol") { + return "name" in this._filterSet[aIndex] + ? this._filterSet[aIndex].name + : ""; + } + return ""; + }, + + _selection: null, + get selection() { + return this._selection; + }, + set selection(val) { + this._selection = val; + }, + getRowProperties(aRow) { + return ""; + }, + getCellProperties(aRow, aColumn) { + return ""; + }, + getColumnProperties(aColumn) { + return ""; + }, + isContainer(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) { + return false; + } + return item.container; + } + return false; + }, + isContainerOpen(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) { + return false; + } + return item.open; + } + return false; + }, + isContainerEmpty(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) { + return false; + } + return item.cookies.length == 0; + } + return false; + }, + isSeparator(aIndex) { + return false; + }, + isSorted(aIndex) { + return false; + }, + canDrop(aIndex, aOrientation) { + return false; + }, + drop(aIndex, aOrientation) {}, + getParentIndex(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + // If an item has no parent index (i.e. it is at the top level) this + // function MUST return -1 otherwise we will go into an infinite loop. + // Containers are always top level items in the cookies tree, so make + // sure to return the appropriate value here. + if (!item || item.container) { + return -1; + } + return item.parentIndex; + } + return -1; + }, + hasNextSibling(aParentIndex, aIndex) { + if (!this._filtered) { + // |aParentIndex| appears to be bogus, but we can get the real + // parent index by getting the entry for |aIndex| and reading the + // parentIndex field. + // The index of the last item in this host collection is the + // index of the parent + the size of the host collection, and + // aIndex has a next sibling if it is less than this value. + var item = this._getItemAtIndex(aIndex); + if (item) { + if (item.container) { + for (var i = aIndex + 1; i < this.rowCount; ++i) { + var subsequent = this._getItemAtIndex(i); + if (subsequent.container) { + return true; + } + } + return false; + } + let parent = this._getItemAtIndex(item.parentIndex); + if (parent && parent.container) { + return aIndex < item.parentIndex + parent.cookies.length; + } + } + } + return aIndex < this.rowCount - 1; + }, + hasPreviousSibling(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) { + return false; + } + var parent = this._getItemAtIndex(item.parentIndex); + if (parent && parent.container) { + return aIndex > item.parentIndex + 1; + } + } + return aIndex > 0; + }, + getLevel(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) { + return 0; + } + return item.level; + } + return 0; + }, + getImageSrc(aIndex, aColumn) {}, + getProgressMode(aIndex, aColumn) {}, + getCellValue(aIndex, aColumn) {}, + setTree(aTree) {}, + toggleOpenState(aIndex) { + if (!this._filtered) { + var item = this._getItemAtIndex(aIndex); + if (!item) { + return; + } + this._invalidateCache(aIndex); + var multiplier = item.open ? -1 : 1; + var delta = multiplier * item.cookies.length; + this._rowCount += delta; + item.open = !item.open; + gCookiesWindow._tree.rowCountChanged(aIndex + 1, delta); + gCookiesWindow._tree.invalidateRow(aIndex); + } + }, + cycleHeader(aColumn) {}, + selectionChanged() {}, + cycleCell(aIndex, aColumn) {}, + isEditable(aIndex, aColumn) { + return false; + }, + setCellValue(aIndex, aColumn, aValue) {}, + setCellText(aIndex, aColumn, aValue) {}, + }, + + _makeStrippedHost(aHost) { + let formattedHost = aHost.startsWith(".") + ? aHost.substring(1, aHost.length) + : aHost; + return formattedHost.startsWith("www.") + ? formattedHost.substring(4, formattedHost.length) + : formattedHost; + }, + + _addCookie(aStrippedHost, aCookie, aHostCount) { + if (!(aStrippedHost in this._hosts) || !this._hosts[aStrippedHost]) { + this._hosts[aStrippedHost] = { + cookies: [], + rawHost: aStrippedHost, + level: 0, + open: false, + container: true, + }; + this._hostOrder.push(aStrippedHost); + ++aHostCount.value; + } + + var c = this._makeCookieObject(aStrippedHost, aCookie); + this._hosts[aStrippedHost].cookies.push(c); + }, + + _makeCookieObject(aStrippedHost, aCookie) { + let c = { + name: aCookie.name, + value: aCookie.value, + isDomain: aCookie.isDomain, + host: aCookie.host, + rawHost: aStrippedHost, + path: aCookie.path, + isSecure: aCookie.isSecure, + expires: aCookie.expires, + level: 1, + container: false, + originAttributes: aCookie.originAttributes, + }; + return c; + }, + + _loadCookies() { + var hostCount = { value: 0 }; + this._hosts = {}; + this._hostOrder = []; + for (let cookie of Services.cookies.cookies) { + var strippedHost = this._makeStrippedHost(cookie.host); + this._addCookie(strippedHost, cookie, hostCount); + } + this._view._rowCount = hostCount.value; + }, + + formatExpiresString(aExpires) { + if (aExpires) { + var date = new Date(1000 * aExpires); + const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "long", + timeStyle: "long", + }); + return dateTimeFormatter.format(date); + } + return this._bundle.getString("expireAtEndOfSession"); + }, + + _getUserContextString(aUserContextId) { + if (parseInt(aUserContextId, 10) == 0) { + return this._bundle.getString("defaultUserContextLabel"); + } + + return ContextualIdentityService.getUserContextLabel(aUserContextId); + }, + + _updateCookieData(aItem) { + var seln = this._view.selection; + var ids = [ + "name", + "value", + "host", + "path", + "isSecure", + "expires", + "userContext", + ]; + var properties; + + if (aItem && !aItem.container && seln.count > 0) { + properties = { + name: aItem.name, + value: aItem.value, + host: aItem.host, + path: aItem.path, + expires: this.formatExpiresString(aItem.expires), + isDomain: aItem.isDomain + ? this._bundle.getString("domainColon") + : this._bundle.getString("hostColon"), + isSecure: aItem.isSecure + ? this._bundle.getString("forSecureOnly") + : this._bundle.getString("forAnyConnection"), + userContext: this._getUserContextString( + aItem.originAttributes.userContextId + ), + }; + for (var i = 0; i < ids.length; ++i) { + document.getElementById(ids[i]).disabled = false; + } + } else { + var noneSelected = this._bundle.getString("noCookieSelected"); + properties = { + name: noneSelected, + value: noneSelected, + host: noneSelected, + path: noneSelected, + expires: noneSelected, + isSecure: noneSelected, + userContext: noneSelected, + }; + for (i = 0; i < ids.length; ++i) { + document.getElementById(ids[i]).disabled = true; + } + } + for (var property in properties) { + document.getElementById(property).value = properties[property]; + } + }, + + onCookieSelected() { + var item; + var seln = this._tree.view.selection; + if (!this._view._filtered) { + item = this._view._getItemAtIndex(seln.currentIndex); + } else { + item = this._view._filterSet[seln.currentIndex]; + } + + this._updateCookieData(item); + + var rangeCount = seln.getRangeCount(); + var selectedCookieCount = 0; + for (var i = 0; i < rangeCount; ++i) { + var min = {}; + var max = {}; + seln.getRangeAt(i, min, max); + for (var j = min.value; j <= max.value; ++j) { + item = this._view._getItemAtIndex(j); + if (!item) { + continue; + } + if (item.container && !item.open) { + selectedCookieCount += item.cookies.length; + } else if (!item.container) { + ++selectedCookieCount; + } + } + } + item = this._view._getItemAtIndex(seln.currentIndex); + if (item && seln.count == 1 && item.container && item.open) { + selectedCookieCount += 2; + } + + let buttonLabel = this._bundle.getString("removeSelectedCookies"); + let removeSelectedCookies = document.getElementById( + "removeSelectedCookies" + ); + removeSelectedCookies.label = PluralForm.get( + selectedCookieCount, + buttonLabel + ).replace("#1", selectedCookieCount); + + removeSelectedCookies.disabled = !(seln.count > 0); + document.getElementById("removeAllCookies").disabled = this._view._filtered; + }, + + deleteCookie() { + // Selection Notes + // - Selection always moves to *NEXT* adjacent item unless item + // is last child at a given level in which case it moves to *PREVIOUS* + // item + // + // Selection Cases (Somewhat Complicated) + // + // 1) Single cookie selected, host has single child + // v cnn.com + // //// cnn.com ///////////// goksdjf@ //// + // > atwola.com + // + // Before SelectedIndex: 1 Before RowCount: 3 + // After SelectedIndex: 0 After RowCount: 1 + // + // 2) Host selected, host open + // v goats.com //////////////////////////// + // goats.com sldkkfjl + // goat.scom flksj133 + // > atwola.com + // + // Before SelectedIndex: 0 Before RowCount: 4 + // After SelectedIndex: 0 After RowCount: 1 + // + // 3) Host selected, host closed + // > goats.com //////////////////////////// + // > atwola.com + // + // Before SelectedIndex: 0 Before RowCount: 2 + // After SelectedIndex: 0 After RowCount: 1 + // + // 4) Single cookie selected, host has many children + // v goats.com + // goats.com sldkkfjl + // //// goats.com /////////// flksjl33 //// + // > atwola.com + // + // Before SelectedIndex: 2 Before RowCount: 4 + // After SelectedIndex: 1 After RowCount: 3 + // + // 5) Single cookie selected, host has many children + // v goats.com + // //// goats.com /////////// flksjl33 //// + // goats.com sldkkfjl + // > atwola.com + // + // Before SelectedIndex: 1 Before RowCount: 4 + // After SelectedIndex: 1 After RowCount: 3 + var seln = this._view.selection; + var tbo = this._tree; + + if (seln.count < 1) { + return; + } + + var nextSelected = 0; + var rowCountImpact = 0; + var deleteItems = []; + if (!this._view._filtered) { + var ci = seln.currentIndex; + nextSelected = ci; + var invalidateRow = -1; + let item = this._view._getItemAtIndex(ci); + if (item.container) { + rowCountImpact -= (item.open ? item.cookies.length : 0) + 1; + deleteItems = deleteItems.concat(item.cookies); + if (!this._view.hasNextSibling(-1, ci)) { + --nextSelected; + } + this._view._removeItemAtIndex(ci); + } else { + var parent = this._view._getItemAtIndex(item.parentIndex); + --rowCountImpact; + if (parent.cookies.length == 1) { + --rowCountImpact; + deleteItems.push(item); + if (!this._view.hasNextSibling(-1, ci)) { + --nextSelected; + } + if (!this._view.hasNextSibling(-1, item.parentIndex)) { + --nextSelected; + } + this._view._removeItemAtIndex(item.parentIndex); + invalidateRow = item.parentIndex; + } else { + deleteItems.push(item); + if (!this._view.hasNextSibling(-1, ci)) { + --nextSelected; + } + this._view._removeItemAtIndex(ci); + } + } + this._view._rowCount += rowCountImpact; + tbo.rowCountChanged(ci, rowCountImpact); + if (invalidateRow != -1) { + tbo.invalidateRow(invalidateRow); + } + } else { + var rangeCount = seln.getRangeCount(); + for (var i = 0; i < rangeCount; ++i) { + var min = {}; + var max = {}; + seln.getRangeAt(i, min, max); + nextSelected = min.value; + for (var j = min.value; j <= max.value; ++j) { + deleteItems.push(this._view._getItemAtIndex(j)); + if (!this._view.hasNextSibling(-1, max.value)) { + --nextSelected; + } + } + var delta = max.value - min.value + 1; + this._view._removeItemAtIndex(min.value, delta); + rowCountImpact = -1 * delta; + this._view._rowCount += rowCountImpact; + tbo.rowCountChanged(min.value, rowCountImpact); + } + } + + for (let item of deleteItems) { + Services.cookies.remove( + item.host, + item.name, + item.path, + item.originAttributes + ); + } + + if (nextSelected < 0) { + seln.clearSelection(); + } else { + seln.select(nextSelected); + this._tree.focus(); + } + }, + + deleteAllCookies() { + Services.cookies.removeAll(); + this._tree.focus(); + }, + + onCookieKeyPress(aEvent) { + if (aEvent.keyCode == 46) { + this.deleteCookie(); + } + }, + + _lastSortProperty: "", + _lastSortAscending: false, + sort(aProperty) { + var ascending = + aProperty == this._lastSortProperty ? !this._lastSortAscending : true; + + function sortByHost(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + } + + // Sort the Non-Filtered Host Collections + if (aProperty == "rawHost") { + this._hostOrder.sort(sortByHost); + if (!ascending) { + this._hostOrder.reverse(); + } + } + + function sortByProperty(a, b) { + return a[aProperty] + .toLowerCase() + .localeCompare(b[aProperty].toLowerCase()); + } + for (var host in this._hosts) { + var cookies = this._hosts[host].cookies; + cookies.sort(sortByProperty); + if (!ascending) { + cookies.reverse(); + } + } + // Sort the Filtered List, if in Filtered mode + if (this._view._filtered) { + this._view._filterSet.sort(sortByProperty); + if (!ascending) { + this._view._filterSet.reverse(); + } + } + + this._view._invalidateCache(0); + this._view.selection.clearSelection(); + this._view.selection.select(0); + this._tree.invalidate(); + this._tree.ensureRowIsVisible(0); + + this._lastSortAscending = ascending; + this._lastSortProperty = aProperty; + }, + + clearFilter() { + // Revert to single-select in the tree + this._tree.setAttribute("seltype", "single"); + + // Clear the Tree Display + this._view._filtered = false; + this._view._rowCount = 0; + this._tree.rowCountChanged(0, -this._view._filterSet.length); + this._view._filterSet = []; + + // Just reload the list to make sure deletions are respected + this._loadCookies(); + this._tree.view = this._view; + + // Restore sort order + var sortby = this._lastSortProperty; + if (sortby == "") { + this._lastSortAscending = false; + this.sort("rawHost"); + } else { + this._lastSortAscending = !this._lastSortAscending; + this.sort(sortby); + } + + // Restore open state + for (var i = 0; i < this._openIndices.length; ++i) { + this._view.toggleOpenState(this._openIndices[i]); + } + this._openIndices = []; + + // Restore selection + this._view.selection.clearSelection(); + for (i = 0; i < this._lastSelectedRanges.length; ++i) { + var range = this._lastSelectedRanges[i]; + this._view.selection.rangedSelect(range.min, range.max, true); + } + this._lastSelectedRanges = []; + + document.getElementById("cookiesIntro").value = + this._bundle.getString("cookiesAll"); + }, + + _cookieMatchesFilter(aCookie) { + return ( + aCookie.rawHost.includes(this._view._filterValue) || + aCookie.name.includes(this._view._filterValue) || + aCookie.value.includes(this._view._filterValue) + ); + }, + + _filterCookies(aFilterValue) { + this._view._filterValue = aFilterValue; + var cookies = []; + for (let i = 0; i < gCookiesWindow._hostOrder.length; ++i) { + let currHost = gCookiesWindow._hosts[gCookiesWindow._hostOrder[i]]; + if (!currHost) { + continue; + } + for (var j = 0; j < currHost.cookies.length; ++j) { + var cookie = currHost.cookies[j]; + if (this._cookieMatchesFilter(cookie)) { + cookies.push(cookie); + } + } + } + return cookies; + }, + + _lastSelectedRanges: [], + _openIndices: [], + _saveState() { + // Save selection + var seln = this._view.selection; + this._lastSelectedRanges = []; + var rangeCount = seln.getRangeCount(); + for (var i = 0; i < rangeCount; ++i) { + var min = {}; + var max = {}; + seln.getRangeAt(i, min, max); + this._lastSelectedRanges.push({ min: min.value, max: max.value }); + } + + // Save open states + this._openIndices = []; + for (i = 0; i < this._view.rowCount; ++i) { + var item = this._view._getItemAtIndex(i); + if (item && item.container && item.open) { + this._openIndices.push(i); + } + } + }, + + filter() { + var filter = document.getElementById("filter").value; + if (filter == "") { + gCookiesWindow.clearFilter(); + return; + } + var view = gCookiesWindow._view; + view._filterSet = gCookiesWindow._filterCookies(filter); + if (!view._filtered) { + // Save Display Info for the Non-Filtered mode when we first + // enter Filtered mode. + gCookiesWindow._saveState(); + view._filtered = true; + } + // Move to multi-select in the tree + gCookiesWindow._tree.setAttribute("seltype", "multiple"); + + // Clear the display + var oldCount = view._rowCount; + view._rowCount = 0; + gCookiesWindow._tree.rowCountChanged(0, -oldCount); + // Set up the filtered display + view._rowCount = view._filterSet.length; + gCookiesWindow._tree.rowCountChanged(0, view.rowCount); + + // if the view is not empty then select the first item + if (view.rowCount > 0) { + view.selection.select(0); + } + + document.getElementById("cookiesIntro").value = + gCookiesWindow._bundle.getString("cookiesFiltered"); + }, + + setFilter(aFilterString) { + document.getElementById("filter").value = aFilterString; + this.filter(); + }, + + focusFilterBox() { + var filter = document.getElementById("filter"); + filter.focus(); + filter.select(); + }, +}; diff --git a/comm/mail/components/preferences/cookies.xhtml b/comm/mail/components/preferences/cookies.xhtml new file mode 100644 index 0000000000..63b6ba5a64 --- /dev/null +++ b/comm/mail/components/preferences/cookies.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?> + +<!DOCTYPE dialog> + +<window id="CookiesDialog" + class="windowDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="cookies-window-dialog2" + onload="gCookiesWindow.init();" + onunload="gCookiesWindow.uninit();" + persist="width height"> + + <script src="chrome://messenger/content/globalOverlay.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + <script src="chrome://messenger/content/preferences/cookies.js"/> + + <stringbundle id="bundlePreferences" + src="chrome://messenger/locale/preferences/preferences.properties"/> + + <linkset> + <html:link rel="localization" href="messenger/preferences/cookies.ftl"/> + </linkset> + + <keyset> + <key data-l10n-id="window-close-key" data-l10n-attrs="key" + modifiers="accel" oncommand="window.close();"/> + <key data-l10n-id="window-focus-search-key" data-l10n-attrs="key" + modifiers="accel" oncommand="gCookiesWindow.focusFilterBox();"/> + <key data-l10n-id="window-focus-search-alt-key" data-l10n-attrs="key" + modifiers="accel" oncommand="gCookiesWindow.focusFilterBox();"/> + </keyset> + + <vbox flex="1" class="contentPane largeDialogContainer"> + <hbox align="center"> + <label data-l10n-id="filter-search-label" control="filter"/> + <search-textbox id="filter" + flex="1" + aria-controls="cookiesList" + oncommand="gCookiesWindow.filter();"/> + </hbox> + <separator class="thin"/> + <label control="cookiesList" id="cookiesIntro" data-l10n-id="cookies-on-system-label"/> + <separator class="thin"/> + <tree id="cookiesList" flex="1" style="height: 10em;" + onkeypress="gCookiesWindow.onCookieKeyPress(event)" + onselect="gCookiesWindow.onCookieSelected();" + hidecolumnpicker="true" seltype="single"> + <treecols> + <treecol id="domainCol" data-l10n-id="treecol-site-header" + primary="true" + persist="width" + onclick="gCookiesWindow.sort('rawHost');"/> + <splitter class="tree-splitter"/> + <treecol id="nameCol" data-l10n-id="treecol-name-header" + persist="width" + onclick="gCookiesWindow.sort('name');"/> + </treecols> + <treechildren id="cookiesChildren"/> + </tree> + <hbox id="cookieInfoSettings" flex="1"> + <vbox> + <vbox flex="1" pack="center" align="end"> + <label id="nameLabel" control="name" data-l10n-id="props-name-label"/> + </vbox> + <vbox flex="1" pack="center" align="end"> + <label id="valueLabel" control="value" data-l10n-id="props-value-label"/> + </vbox> + <vbox flex="1" pack="center" align="end"> + <label id="isDomain" control="host" data-l10n-id="props-domain-label"/> + </vbox> + <vbox flex="1" pack="center" align="end"> + <label id="pathLabel" control="path" data-l10n-id="props-path-label"/> + </vbox> + <vbox flex="1" pack="center" align="end"> + <label id="isSecureLabel" control="isSecure" data-l10n-id="props-secure-label"/> + </vbox> + <vbox flex="1" pack="center" align="end"> + <label id="expiresLabel" control="expires" data-l10n-id="props-expires-label"/> + </vbox> + <vbox id="userContextLabel" flex="1" pack="center" align="end"> + <label control="userContext" data-l10n-id="props-container-label"/> + </vbox> + </vbox> + <vbox flex="1"> + <html:input id="name" type="text" readonly="readonly"/> + <html:input id="value" type="text" readonly="readonly"/> + <html:input id="host" type="text" readonly="readonly"/> + <html:input id="path" type="text" readonly="readonly"/> + <html:input id="isSecure" type="text" readonly="readonly"/> + <html:input id="expires" type="text" readonly="readonly"/> + <html:input id="userContext" type="text" readonly="readonly"/> + </vbox> + </hbox> + </vbox> + <hbox align="end"> + <hbox class="actionButtons" flex="1"> + <button id="removeSelectedCookies" disabled="true" + data-l10n-id="remove-cookie-button" + oncommand="gCookiesWindow.deleteCookie();"/> + <button id="removeAllCookies" disabled="true" + data-l10n-id="remove-all-cookies-button" + oncommand="gCookiesWindow.deleteAllCookies();"/> + <spacer flex="1"/> +#ifndef XP_MACOSX + <button oncommand="window.close();" + data-l10n-id="cookie-close-button"/> +#endif + </hbox> + </hbox> +</window> diff --git a/comm/mail/components/preferences/dockoptions.js b/comm/mail/components/preferences/dockoptions.js new file mode 100644 index 0000000000..d518150551 --- /dev/null +++ b/comm/mail/components/preferences/dockoptions.js @@ -0,0 +1,11 @@ +/* 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 ../../../../toolkit/content/preferencesBindings.js */ + +Preferences.addAll([ + { id: "mail.biff.animate_dock_icon", type: "bool" }, + { id: "mail.biff.show_badge", type: "bool" }, + { id: "mail.biff.use_new_count_in_badge", type: "bool" }, +]); diff --git a/comm/mail/components/preferences/dockoptions.xhtml b/comm/mail/components/preferences/dockoptions.xhtml new file mode 100644 index 0000000000..978cbe1e2d --- /dev/null +++ b/comm/mail/components/preferences/dockoptions.xhtml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<!DOCTYPE window> + +<window type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="dock-options-window-dialog2" + style="min-width: 33em;"> + <dialog id="DockOptionsDialog" + dlgbuttons="accept,cancel"> + + <linkset> + <html:link rel="localization" href="messenger/preferences/dock-options.ftl"/> + </linkset> + <hbox orient="vertical"> +#ifdef XP_MACOSX + <checkbox id="newMailNotificationBounce" + data-l10n-id="bounce-system-dock-icon" + preference="mail.biff.animate_dock_icon"/> +#endif +#ifdef XP_WIN + <checkbox id="newMailBadge" + data-l10n-id="dock-options-show-badge" + preference="mail.biff.show_badge"/> +#endif + <separator class="thin"/> + <html:div> + <html:fieldset> + <html:legend data-l10n-id="dock-icon-legend"></html:legend> + <vbox> + <separator class="thin"/> + <label data-l10n-id="dock-icon-show-label"/> + <radiogroup id="dockCount" + preference="mail.biff.use_new_count_in_badge" + class="indent" orient="vertical"> + <radio id="dockCountAll" value="false" + data-l10n-id="count-unread-messages-radio"/> + <radio id="dockCountNew" value="true" + data-l10n-id="count-new-messages-radio"/> + </radiogroup> + </vbox> + </html:fieldset> + </html:div> +#ifdef XP_MACOSX + <separator/> + <description class="bold" data-l10n-id="notification-settings-info2"/> +#endif + </hbox> + + <script src="chrome://global/content/preferencesBindings.js"/> + <script src="chrome://messenger/content/preferences/dockoptions.js"/> + </dialog> +</window> diff --git a/comm/mail/components/preferences/downloads.js b/comm/mail/components/preferences/downloads.js new file mode 100644 index 0000000000..ede1543492 --- /dev/null +++ b/comm/mail/components/preferences/downloads.js @@ -0,0 +1,132 @@ +/* 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 preferences.js */ + +var { Downloads } = ChromeUtils.importESModule( + "resource://gre/modules/Downloads.sys.mjs" +); +var { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); + +Preferences.addAll([ + { id: "browser.download.useDownloadDir", type: "bool" }, + { id: "browser.download.folderList", type: "int" }, + { id: "browser.download.downloadDir", type: "file" }, + { id: "browser.download.dir", type: "file" }, + { id: "pref.downloads.disable_button.edit_actions", type: "bool" }, +]); + +var gDownloadDirSection = { + async chooseFolder() { + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + var bundlePreferences = document.getElementById("bundlePreferences"); + var title = bundlePreferences.getString("chooseAttachmentsFolderTitle"); + fp.init(window, title, Ci.nsIFilePicker.modeGetFolder); + + var customDirPref = Preferences.get("browser.download.dir"); + if (customDirPref.value) { + fp.displayDirectory = customDirPref.value; + } + fp.appendFilters(Ci.nsIFilePicker.filterAll); + + let rv = await new Promise(resolve => fp.open(resolve)); + if (rv != Ci.nsIFilePicker.returnOK || !fp.file) { + return; + } + + let file = fp.file.QueryInterface(Ci.nsIFile); + let currentDirPref = Preferences.get("browser.download.downloadDir"); + customDirPref.value = currentDirPref.value = file; + let folderListPref = Preferences.get("browser.download.folderList"); + folderListPref.value = await this._fileToIndex(file); + }, + + onReadUseDownloadDir() { + this.readDownloadDirPref(); + var downloadFolder = document.getElementById("downloadFolder"); + var chooseFolder = document.getElementById("chooseFolder"); + var preference = Preferences.get("browser.download.useDownloadDir"); + var dirPreference = Preferences.get("browser.download.dir"); + downloadFolder.disabled = !preference.value || dirPreference.locked; + chooseFolder.disabled = !preference.value || dirPreference.locked; + return undefined; + }, + + async _fileToIndex(aFile) { + if (!aFile || aFile.equals(await this._getDownloadsFolder("Desktop"))) { + return 0; + } else if (aFile.equals(await this._getDownloadsFolder("Downloads"))) { + return 1; + } + return 2; + }, + + async _indexToFile(aIndex) { + switch (aIndex) { + case 0: + return this._getDownloadsFolder("Desktop"); + case 1: + return this._getDownloadsFolder("Downloads"); + } + var customDirPref = Preferences.get("browser.download.dir"); + return customDirPref.value; + }, + + 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'" + ); + }, + + async readDownloadDirPref() { + var folderListPref = Preferences.get("browser.download.folderList"); + var bundlePreferences = document.getElementById("bundlePreferences"); + var downloadFolder = document.getElementById("downloadFolder"); + + var customDirPref = Preferences.get("browser.download.dir"); + var customIndex = customDirPref.value + ? await this._fileToIndex(customDirPref.value) + : 0; + if (customIndex == 0) { + downloadFolder.value = bundlePreferences.getString("desktopFolderName"); + } else if (customIndex == 1) { + downloadFolder.value = bundlePreferences.getString( + "myDownloadsFolderName" + ); + } else { + downloadFolder.value = customDirPref.value + ? customDirPref.value.path + : ""; + } + + var currentDirPref = Preferences.get("browser.download.downloadDir"); + var downloadDir = + currentDirPref.value || (await this._indexToFile(folderListPref.value)); + if (downloadDir) { + let urlSpec = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler) + .getURLSpecFromDir(downloadDir); + + downloadFolder.style.backgroundImage = + "url(moz-icon://" + urlSpec + "?size=16)"; + } + + return undefined; + }, +}; + +Preferences.get("browser.download.dir").on( + "change", + gDownloadDirSection.readDownloadDirPref.bind(gDownloadDirSection) +); diff --git a/comm/mail/components/preferences/extensionControlled.js b/comm/mail/components/preferences/extensionControlled.js new file mode 100644 index 0000000000..5dccb348bc --- /dev/null +++ b/comm/mail/components/preferences/extensionControlled.js @@ -0,0 +1,129 @@ +/* - 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 preferences.js */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + ExtensionSettingsStore: + "resource://gre/modules/ExtensionSettingsStore.sys.mjs", +}); + +const API_PROXY_PREFS = [ + "network.proxy.type", + "network.proxy.http", + "network.proxy.http_port", + "network.proxy.share_proxy_settings", + "network.proxy.ssl", + "network.proxy.ssl_port", + "network.proxy.socks", + "network.proxy.socks_port", + "network.proxy.socks_version", + "network.proxy.socks_remote_dns", + "network.proxy.no_proxies_on", + "network.proxy.autoconfig_url", + "signon.autologin.proxy", +]; + +/** + * Check if a pref is being managed by an extension. + * + * NOTE: We only currently handle proxy.settings. + */ +/** + * Get the addon extension that is controlling the proxy settings. + * + * @returns - The found addon, or undefined if none was found. + */ +async function getControllingProxyExtensionAddon() { + await ExtensionSettingsStore.initialize(); + let id = ExtensionSettingsStore.getSetting("prefs", "proxy.settings")?.id; + if (id) { + return AddonManager.getAddonByID(id); + } + return undefined; +} + +/** + * Show or hide the proxy extension message depending on whether or not the + * proxy settings are controlled by an extension. + * + * @returns {boolean} - Whether the proxy settings are controlled by an + * extension. + */ +async function handleControllingProxyExtension() { + let addon = await getControllingProxyExtensionAddon(); + if (addon) { + showControllingProxyExtension(addon); + } else { + hideControllingProxyExtension(); + } + return !!addon; +} + +/** + * Show the proxy extension message. + * + * @param {object} addon - The addon extension that is currently controlling the + * proxy settings. + * @param {string} addon.name - The addon name. + * @param {string} [addon.iconUrl] - The addon icon source. + */ +function showControllingProxyExtension(addon) { + let description = document.getElementById("proxyExtensionDescription"); + description + .querySelector("img") + .setAttribute( + "src", + addon.iconUrl || "chrome://mozapps/skin/extensions/extensionGeneric.svg" + ); + document.l10n.setAttributes( + description, + "proxy-settings-controlled-by-extension", + { name: addon.name } + ); + + document.getElementById("proxyExtensionContent").hidden = false; +} + +/** + * Hide the proxy extension message. + */ +function hideControllingProxyExtension() { + document.getElementById("proxyExtensionContent").hidden = true; +} + +/** + * Disable the addon extension that is currently controlling the proxy settings. + */ +function disableControllingProxyExtension() { + getControllingProxyExtensionAddon().then(addon => addon?.disable()); +} + +/** + * Start listening to the proxy settings, and update the UI accordingly. + * + * @param {object} container - The proxy container. + * @param {Function} container.updateProxySettingsUI - A callback to call + * whenever the proxy settings change. + */ +function initializeProxyUI(container) { + let deferredUpdate = new DeferredTask(() => { + container.updateProxySettingsUI(); + }, 10); + let proxyObserver = { + observe: (subject, topic, data) => { + if (API_PROXY_PREFS.includes(data)) { + deferredUpdate.arm(); + } + }, + }; + Services.prefs.addObserver("", proxyObserver); + window.addEventListener("unload", () => { + Services.prefs.removeObserver("", proxyObserver); + }); +} diff --git a/comm/mail/components/preferences/findInPage.js b/comm/mail/components/preferences/findInPage.js new file mode 100644 index 0000000000..c69e8b50b6 --- /dev/null +++ b/comm/mail/components/preferences/findInPage.js @@ -0,0 +1,641 @@ +/* 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 */ + +// A tweak to the standard <button> CE to use textContent on the <label> +// inside the button, which allows the text to be highlighted when the user +// is searching. + +const MozButton = customElements.get("button"); +class HighlightableButton extends MozButton { + static get inheritedAttributes() { + return Object.assign({}, super.inheritedAttributes, { + ".button-text": "text=label,accesskey,crop", + }); + } +} +customElements.define("highlightable-button", HighlightableButton, { + extends: "button", +}); + +var gSearchResultsPane = { + listSearchTooltips: new Set(), + listSearchMenuitemIndicators: new Set(), + searchInput: null, + // A map of DOM Elements to a string of keywords used in search. + // XXX: We should invalidate this cache on `intl:app-locales-changed`. + searchKeywords: new WeakMap(), + inited: false, + + init() { + if (this.inited) { + return; + } + this.inited = true; + this.searchInput = document.getElementById("searchInput"); + this.searchInput.hidden = !Services.prefs.getBoolPref( + "browser.preferences.search" + ); + if (!this.searchInput.hidden) { + this.searchInput.addEventListener("input", this); + this.searchInput.addEventListener("command", this); + window.addEventListener("DOMContentLoaded", () => { + this.searchInput.focus(); + }); + // Initialize other panes in an idle callback. + window.requestIdleCallback(() => this.initializeCategories()); + } + let helpUrl = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "preferences"; + let helpContainer = document.getElementById("need-help"); + helpContainer.querySelector("a").href = helpUrl; + }, + + async handleEvent(event) { + // Ensure categories are initialized if idle callback didn't run soon enough. + await this.initializeCategories(); + this.searchFunction(event); + }, + + /** + * Check that the text content contains the query string. + * + * @param {string} content the text content to be searched. + * @param {string} query the query string. + * + * @returns {boolean} true when the text content contains the query string else false. + */ + queryMatchesContent(content, query) { + if (!content || !query) { + return false; + } + return content.toLowerCase().includes(query.toLowerCase()); + }, + + categoriesInitialized: false, + + /** + * Will attempt to initialize all uninitialized categories. + */ + async initializeCategories() { + // Initializing all the JS for all the tabs. + if (!this.categoriesInitialized) { + this.categoriesInitialized = true; + // Each element of gCategoryInits is a name. + for (let [name, category] of gCategoryInits) { + if ( + (name != "paneCalendar" && !category.inited) || + (calendarDeactivator.isCalendarActivated && !category.inited) + ) { + await category.init(); + } + } + let lastSelected = Services.xulStore.getValue( + "about:preferences", + "paneDeck", + "lastSelected" + ); + search(lastSelected, "data-category"); + } + }, + + /** + * Finds and returns text nodes within node and all descendants. + * Iterates through all the sibilings of the node object and adds the sibilings + * to an array if sibling is a TEXT_NODE else checks the text nodes with in current node. + * Source - http://stackoverflow.com/questions/10730309/find-all-text-nodes-in-html-page + * + * @param {Node} node DOM element. + * + * @returns {Node[]} array of text nodes. + */ + textNodeDescendants(node) { + if (!node) { + return []; + } + let all = []; + for (node = node.firstChild; node; node = node.nextSibling) { + if (node.nodeType === node.TEXT_NODE) { + all.push(node); + } else { + all = all.concat(this.textNodeDescendants(node)); + } + } + return all; + }, + + /** + * This function is used to find words contained within the text nodes. + * We pass in the textNodes because they contain the text to be highlighted. + * We pass in the nodeSizes to tell exactly where highlighting need be done. + * When creating the range for highlighting, if the nodes are section is split + * by an access key, it is important to have the size of each of the nodes summed. + * + * @param {Node[]} textNodes List of DOM elements. + * @param {Node[]} nodeSizes Running size of text nodes. This will contain the same + * number of elements as textNodes. The first element is the size of first textNode element. + * For any nodes after, they will contain the summation of the nodes thus far in the array. + * Example: + * textNodes = [[This is ], [a], [n example]] + * nodeSizes = [[8], [9], [18]] + * This is used to determine the offset when highlighting. + * @param {string} textSearch Concatenation of textNodes's text content. + * Example: + * textNodes = [[This is ], [a], [n example]] + * nodeSizes = "This is an example" + * This is used when executing the regular expression. + * @param {string} searchPhrase word or words to search for. + * + * @returns {boolean} Returns true when atleast one instance of search phrase is found, otherwise false. + */ + highlightMatches(textNodes, nodeSizes, textSearch, searchPhrase) { + if (!searchPhrase) { + return false; + } + + let indices = []; + let i = -1; + while ((i = textSearch.indexOf(searchPhrase, i + 1)) >= 0) { + indices.push(i); + } + + // Looping through each spot the searchPhrase is found in the concatenated string.dom-mutation-list. + for (let startValue of indices) { + let endValue = startValue + searchPhrase.length; + let startNode = null; + let endNode = null; + let nodeStartIndex = null; + + // Determining the start and end node to highlight from. + for (let index = 0; index < nodeSizes.length; index++) { + let lengthNodes = nodeSizes[index]; + // Determining the start node. + if (!startNode && lengthNodes >= startValue) { + startNode = textNodes[index]; + nodeStartIndex = index; + // Calculating the offset when found query is not in the first node. + if (index > 0) { + startValue -= nodeSizes[index - 1]; + } + } + // Determining the end node. + if (!endNode && lengthNodes >= endValue) { + endNode = textNodes[index]; + // Calculating the offset when endNode is different from startNode + // or when endNode is not the first node. + if (index != nodeStartIndex || index > 0) { + endValue -= nodeSizes[index - 1]; + } + } + } + let range = document.createRange(); + range.setStart(startNode, startValue); + range.setEnd(endNode, endValue); + this.getFindSelection(startNode.ownerGlobal).addRange(range); + } + + return !!indices.length; + }, + + /** + * Get the selection instance from given window. + * + * @param {object} win The window object points to frame's window. + */ + getFindSelection(win) { + // Yuck. See bug 138068. + let docShell = win.docShell; + + let controller = docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsISelectionDisplay) + .QueryInterface(Ci.nsISelectionController); + + let selection = controller.getSelection( + Ci.nsISelectionController.SELECTION_FIND + ); + selection.setColors("currentColor", "#ffe900", "currentColor", "#003eaa"); + + return selection; + }, + + /** + * Shows or hides content according to search input. + * + * @param {object} event to search for filted query in. + */ + async searchFunction(event) { + let query = event.target.value.trim().toLowerCase(); + if (this.query == query) { + return; + } + + let subQuery = this.query && query.includes(this.query); + this.query = query; + + this.getFindSelection(window).removeAllRanges(); + this.removeAllSearchTooltips(); + this.removeAllSearchMenuitemIndicators(); + + let srHeader = document.getElementById("header-searchResults"); + let noResultsEl = document.getElementById("no-results-message"); + if (this.query) { + // Showing the Search Results Tag. + await gotoPref("paneSearchResults"); + srHeader.hidden = false; + + let resultsFound = false; + + // Building the range for highlighted areas. + let rootPreferencesChildren = [ + ...document.querySelectorAll( + "#paneDeck > *:not([data-hidden-from-search],script,stringbundle,commandset,keyset,linkset)" + ), + ]; + + if (subQuery) { + // Since the previous query is a subset of the current query, + // there is no need to check elements that is hidden already. + rootPreferencesChildren = rootPreferencesChildren.filter( + el => !el.hidden + ); + } + + // Attach the bindings for all children if they were not already visible. + for (let child of rootPreferencesChildren) { + if (child.hidden) { + child.classList.add("visually-hidden"); + child.hidden = false; + } + } + + let ts = performance.now(); + let FRAME_THRESHOLD = 10; + + // Showing or Hiding specific section depending on if words in query are found. + for (let child of rootPreferencesChildren) { + if (performance.now() - ts > FRAME_THRESHOLD) { + // Creating tooltips for all the instances found. + for (let anchorNode of this.listSearchTooltips) { + this.createSearchTooltip(anchorNode, this.query); + } + ts = await new Promise(resolve => + window.requestAnimationFrame(resolve) + ); + if (query !== this.query) { + return; + } + } + + if ( + !child.classList.contains("header") && + !child.classList.contains("subcategory") && + (await this.searchWithinNode(child, this.query)) + ) { + child.classList.remove("visually-hidden"); + + // Show the preceding search-header if one exists. + let groupbox = child.closest("groupbox"); + let groupHeader = + groupbox && groupbox.querySelector(".search-header"); + if (groupHeader) { + groupHeader.hidden = false; + } + + resultsFound = true; + } else { + child.classList.add("visually-hidden"); + } + } + + noResultsEl.hidden = !!resultsFound; + noResultsEl.setAttribute("query", this.query); + // XXX: This is potentially racy in case where Fluent retranslates the + // message and ereases the query within. + // The feature is not yet supported, but we should fix for it before + // we enable it. See bug 1446389 for details. + let msgQueryElem = document.getElementById("sorry-message-query"); + msgQueryElem.textContent = this.query; + if (resultsFound) { + // Creating tooltips for all the instances found. + for (let anchorNode of this.listSearchTooltips) { + this.createSearchTooltip(anchorNode, this.query); + } + } + } else { + noResultsEl.hidden = true; + document.getElementById("sorry-message-query").textContent = ""; + // Going back to General when cleared. + await gotoPref("paneGeneral"); + srHeader.hidden = true; + + // Hide some special second level headers in normal view. + for (let element of document.querySelectorAll(".search-header")) { + element.hidden = true; + } + } + + window.dispatchEvent( + new CustomEvent("PreferencesSearchCompleted", { detail: query }) + ); + }, + + /** + * Finding leaf nodes and checking their content for words to search, + * It is a recursive function. + * + * @param {Node} nodeObject DOM Element. + * @param {string} searchPhrase + * + * @returns {boolean} Returns true when found in at least one childNode, false otherwise. + */ + async searchWithinNode(nodeObject, searchPhrase) { + let matchesFound = false; + if ( + nodeObject.childElementCount == 0 || + nodeObject.tagName == "button" || + nodeObject.tagName == "label" || + nodeObject.tagName == "description" || + nodeObject.tagName == "menulist" || + nodeObject.tagName == "menuitem" + ) { + let simpleTextNodes = this.textNodeDescendants(nodeObject); + for (let node of simpleTextNodes) { + let result = this.highlightMatches( + [node], + [node.length], + node.textContent.toLowerCase(), + searchPhrase + ); + matchesFound = matchesFound || result; + } + + // Collecting data from anonymous content / label / description. + let nodeSizes = []; + let allNodeText = ""; + let runningSize = 0; + + let accessKeyTextNodes = []; + + if ( + nodeObject.tagName == "label" || + nodeObject.tagName == "description" + ) { + accessKeyTextNodes.push(...simpleTextNodes); + } + + for (let node of accessKeyTextNodes) { + runningSize += node.textContent.length; + allNodeText += node.textContent; + nodeSizes.push(runningSize); + } + + // Access key are presented. + let complexTextNodesResult = this.highlightMatches( + accessKeyTextNodes, + nodeSizes, + allNodeText.toLowerCase(), + searchPhrase + ); + + // Searching some elements, such as xul:button, have a 'label' attribute + // that contains the user-visible text. + let labelResult = this.queryMatchesContent( + nodeObject.getAttribute("label"), + searchPhrase + ); + + // Searching some elements, such as xul:label, store their user-visible + // text in a "value" attribute. Value will be skipped for menuitem since + // value in menuitem could represent index number to distinct each item. + let valueResult = + nodeObject.tagName !== "menuitem" && nodeObject.tagName !== "radio" + ? this.queryMatchesContent( + nodeObject.getAttribute("value"), + searchPhrase + ) + : false; + + // Searching some elements, such as xul:button, buttons to open subdialogs + // using l10n ids. + let keywordsResult = + nodeObject.hasAttribute("search-l10n-ids") && + (await this.matchesSearchL10nIDs(nodeObject, searchPhrase)); + + if (!keywordsResult) { + // Searching some elements, such as xul:button, buttons to open subdialogs + // using searchkeywords attribute. + keywordsResult = + !keywordsResult && + nodeObject.hasAttribute("searchkeywords") && + this.queryMatchesContent( + nodeObject.getAttribute("searchkeywords"), + searchPhrase + ); + } + + // Creating tooltips for buttons. + if ( + keywordsResult && + (nodeObject.tagName === "button" || nodeObject.tagName == "menulist") + ) { + this.listSearchTooltips.add(nodeObject); + } + + if (keywordsResult && nodeObject.tagName === "menuitem") { + nodeObject.setAttribute("indicator", "true"); + this.listSearchMenuitemIndicators.add(nodeObject); + let menulist = nodeObject.closest("menulist"); + + menulist.setAttribute("indicator", "true"); + this.listSearchMenuitemIndicators.add(menulist); + } + + if ( + (nodeObject.tagName == "menulist" || + nodeObject.tagName == "menuitem") && + (labelResult || valueResult || keywordsResult) + ) { + nodeObject.setAttribute("highlightable", "true"); + } + + matchesFound = + matchesFound || + complexTextNodesResult || + labelResult || + valueResult || + keywordsResult; + } + + for (let i = 0; i < nodeObject.childNodes.length; i++) { + let result = await this.searchChildNodeIfVisible( + nodeObject, + i, + searchPhrase + ); + matchesFound = matchesFound || result; + } + return matchesFound; + }, + + /** + * Search for a phrase within a child node if it is visible. + * + * @param {Node} nodeObject The parent DOM Element. + * @param {number} index The index for the childNode. + * @param {string} searchPhrase + * + * @returns {boolean} Returns true when found the specific childNode, false otherwise + */ + async searchChildNodeIfVisible(nodeObject, index, searchPhrase) { + let result = false; + if ( + !nodeObject.childNodes[index].hidden && + nodeObject.getAttribute("data-hidden-from-search") !== "true" + ) { + result = await this.searchWithinNode( + nodeObject.childNodes[index], + searchPhrase + ); + // Creating tooltips for menulist element. + if (result && nodeObject.tagName === "menulist") { + this.listSearchTooltips.add(nodeObject); + } + } + return result; + }, + + /** + * Search for a phrase in l10n messages associated with the element. + * + * @param {Node} nodeObject The parent DOM Element. + * @param {string} searchPhrase. + * @returns {boolean} true when the text content contains the query string else false. + */ + async matchesSearchL10nIDs(nodeObject, searchPhrase) { + if (!this.searchKeywords.has(nodeObject)) { + // The `search-l10n-ids` attribute is a comma-separated list of + // l10n ids. It may also uses a dot notation to specify an attribute + // of the message to be used. + // + // Example: "containers-add-button.label, user-context-personal". + // + // The result is an array of arrays of l10n ids and optionally attribute names. + // + // Example: [["containers-add-button", "label"], ["user-context-personal"]] + const refs = nodeObject + .getAttribute("search-l10n-ids") + .split(",") + .map(s => s.trim().split(".")) + .filter(s => !!s[0].length); + + const messages = await document.l10n.formatMessages( + refs.map(ref => ({ id: ref[0] })) + ); + + // Map the localized messages taking value or a selected attribute and + // building a string of concatenated translated strings out of it. + let keywords = messages + .map((msg, i) => { + let [refId, refAttr] = refs[i]; + if (!msg) { + console.error(`Missing search l10n id "${refId}"`); + return null; + } + if (refAttr) { + let attr = + msg.attributes && msg.attributes.find(a => a.name === refAttr); + if (!attr) { + console.error(`Missing search l10n id "${refId}.${refAttr}"`); + return null; + } + if (attr.value === "") { + console.error( + `Empty value added to search-l10n-ids "${refId}.${refAttr}"` + ); + } + return attr.value; + } + if (msg.value === "") { + console.error(`Empty value added to search-l10n-ids "${refId}"`); + } + return msg.value; + }) + .filter(keyword => keyword !== null) + .join(" "); + + this.searchKeywords.set(nodeObject, keywords); + return this.queryMatchesContent(keywords, searchPhrase); + } + + return this.queryMatchesContent( + this.searchKeywords.get(nodeObject), + searchPhrase + ); + }, + + /** + * Inserting a div structure infront of the DOM element matched textContent. + * Then calculation the offsets to position the tooltip in the correct place. + * + * @param {Node} anchorNode DOM Element. + * @param {string} query Word or words that are being searched for. + */ + createSearchTooltip(anchorNode, query) { + if (anchorNode.tooltipNode) { + return; + } + let searchTooltip = anchorNode.ownerDocument.createElement("span"); + let searchTooltipText = anchorNode.ownerDocument.createElement("span"); + searchTooltip.className = "search-tooltip"; + searchTooltipText.textContent = query; + searchTooltip.appendChild(searchTooltipText); + + // Set tooltipNode property to track corresponded tooltip node. + anchorNode.tooltipNode = searchTooltip; + anchorNode.parentElement.classList.add("search-tooltip-parent"); + anchorNode.parentElement.appendChild(searchTooltip); + + this.calculateTooltipPosition(anchorNode); + }, + + calculateTooltipPosition(anchorNode) { + let searchTooltip = anchorNode.tooltipNode; + // In order to get the up-to-date position of each of the nodes that we're + // putting tooltips on, we have to flush layout intentionally, and that + // this is the result of a XUL limitation (bug 1363730). + let tooltipRect = searchTooltip.getBoundingClientRect(); + searchTooltip.style.setProperty( + "left", + `calc(50% - ${tooltipRect.width / 2}px)` + ); + }, + + /** + * Remove all search tooltips. + */ + removeAllSearchTooltips() { + for (let anchorNode of this.listSearchTooltips) { + anchorNode.parentElement.classList.remove("search-tooltip-parent"); + if (anchorNode.tooltipNode) { + anchorNode.tooltipNode.remove(); + } + anchorNode.tooltipNode = null; + } + this.listSearchTooltips.clear(); + }, + + /** + * Remove all indicators on menuitem. + */ + removeAllSearchMenuitemIndicators() { + for (let node of this.listSearchMenuitemIndicators) { + node.removeAttribute("indicator"); + } + this.listSearchMenuitemIndicators.clear(); + }, +}; diff --git a/comm/mail/components/preferences/fonts.js b/comm/mail/components/preferences/fonts.js new file mode 100644 index 0000000000..d6a4c1308e --- /dev/null +++ b/comm/mail/components/preferences/fonts.js @@ -0,0 +1,196 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +// toolkit/content/preferencesBindings.js +/* globals Preferences */ +// toolkit/mozapps/preferences/fontbuilder.js +/* globals FontBuilder */ + +var kDefaultFontType = "font.default.%LANG%"; +var kFontNameFmtSerif = "font.name.serif.%LANG%"; +var kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%"; +var kFontNameFmtMonospace = "font.name.monospace.%LANG%"; +var kFontNameListFmtSerif = "font.name-list.serif.%LANG%"; +var kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%"; +var kFontNameListFmtMonospace = "font.name-list.monospace.%LANG%"; +var kFontSizeFmtVariable = "font.size.variable.%LANG%"; +var kFontSizeFmtFixed = "font.size.monospace.%LANG%"; +var kFontMinSizeFmt = "font.minimum-size.%LANG%"; + +Preferences.addAll([ + { id: "font.language.group", type: "wstring" }, + { id: "browser.display.use_document_fonts", type: "int" }, + { id: "mail.fixed_width_messages", type: "bool" }, +]); + +var gFontsDialog = { + _selectLanguageGroupPromise: Promise.resolve(), + + init() { + Preferences.addSyncFromPrefListener( + document.getElementById("selectLangs"), + () => gFontsDialog.readFontLanguageGroup() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("serif"), + element => FontBuilder.readFontSelection(element) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("sans-serif"), + element => FontBuilder.readFontSelection(element) + ); + Preferences.addSyncFromPrefListener( + document.getElementById("monospace"), + element => FontBuilder.readFontSelection(element) + ); + + let element = document.getElementById("useDocumentFonts"); + Preferences.addSyncFromPrefListener(element, () => + gFontsDialog.readUseDocumentFonts() + ); + Preferences.addSyncToPrefListener(element, () => + gFontsDialog.writeUseDocumentFonts() + ); + + element = document.getElementById("mailFixedWidthMessages"); + Preferences.addSyncFromPrefListener(element, () => + gFontsDialog.readFixedWidthForPlainText() + ); + Preferences.addSyncToPrefListener(element, () => + gFontsDialog.writeFixedWidthForPlainText() + ); + }, + + _selectLanguageGroup(aLanguageGroup) { + this._selectLanguageGroupPromise = (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._selectLanguageGroupPromise; + + var prefs = [ + { + format: kDefaultFontType, + type: "string", + element: "defaultFontType", + fonttype: null, + }, + { + format: kFontNameFmtSerif, + type: "fontname", + element: "serif", + fonttype: "serif", + }, + { + format: kFontNameFmtSansSerif, + type: "fontname", + element: "sans-serif", + fonttype: "sans-serif", + }, + { + format: kFontNameFmtMonospace, + type: "fontname", + element: "monospace", + fonttype: "monospace", + }, + { + format: kFontNameListFmtSerif, + type: "unichar", + element: null, + fonttype: "serif", + }, + { + format: kFontNameListFmtSansSerif, + type: "unichar", + element: null, + fonttype: "sans-serif", + }, + { + format: kFontNameListFmtMonospace, + type: "unichar", + element: null, + fonttype: "monospace", + }, + { + format: kFontSizeFmtVariable, + type: "int", + element: "sizeVar", + fonttype: null, + }, + { + format: kFontSizeFmtFixed, + type: "int", + element: "sizeMono", + fonttype: null, + }, + { + format: kFontMinSizeFmt, + type: "int", + element: "minSize", + fonttype: null, + }, + ]; + for (var i = 0; i < prefs.length; ++i) { + var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup); + var preference = Preferences.get(name); + if (!preference) { + preference = Preferences.add({ id: name, type: prefs[i].type }); + } + + if (!prefs[i].element) { + continue; + } + + var element = document.getElementById(prefs[i].element); + if (element) { + element.setAttribute("preference", preference.id); + + if (prefs[i].fonttype) { + await FontBuilder.buildFontList( + aLanguageGroup, + prefs[i].fonttype, + element + ); + } + preference.setElementValue(element); + } + } + })().catch(console.error); + }, + + readFontLanguageGroup() { + var languagePref = Preferences.get("font.language.group"); + this._selectLanguageGroup(languagePref.value); + return undefined; + }, + + readUseDocumentFonts() { + var preference = Preferences.get("browser.display.use_document_fonts"); + return preference.value == 1; + }, + + writeUseDocumentFonts() { + var useDocumentFonts = document.getElementById("useDocumentFonts"); + return useDocumentFonts.checked ? 1 : 0; + }, + + readFixedWidthForPlainText() { + var preference = Preferences.get("mail.fixed_width_messages"); + return preference.value == 1; + }, + + writeFixedWidthForPlainText() { + var mailFixedWidthMessages = document.getElementById( + "mailFixedWidthMessages" + ); + return mailFixedWidthMessages.checked; + }, +}; + +window.addEventListener("load", () => gFontsDialog.init()); diff --git a/comm/mail/components/preferences/fonts.xhtml b/comm/mail/components/preferences/fonts.xhtml new file mode 100644 index 0000000000..4bb793a04d --- /dev/null +++ b/comm/mail/components/preferences/fonts.xhtml @@ -0,0 +1,337 @@ +<?xml version="1.0"?> +<!-- 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 https://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<!DOCTYPE html> +<html + id="FontsDialog" + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + type="child" + persist="lastSelected" + scrolling="false" + style="min-width: 60ch" +> + <head> + <title data-l10n-id="fonts-dialog-title"></title> + <link rel="localization" href="messenger/preferences/fonts.ftl" /> + <script + defer="defer" + src="chrome://global/content/preferencesBindings.js" + ></script> + <script + defer="defer" + src="chrome://mozapps/content/preferences/fontbuilder.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/menulist-charsetpicker.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/preferences/fonts.js" + ></script> + </head> + <html:body + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + > + <dialog buttons="accept,cancel" style="min-height: 100vh"> + <keyset> + <key + data-l10n-id="fonts-window-close" + modifiers="accel" + oncommand="Preferences.close(event)" + /> + </keyset> + + <html:fieldset> + <!-- title row --> + <hbox> + <hbox align="center" pack="end"> + <label + id="fontsTitle" + control="selectLangs" + data-l10n-id="fonts-language-legend" + /> + </hbox> + <menulist id="selectLangs" flex="1" preference="font.language.group"> + <menupopup> + <menuitem value="ar" data-l10n-id="font-language-group-arabic" /> + <menuitem + value="x-armn" + data-l10n-id="font-language-group-armenian" + /> + <menuitem + value="x-beng" + data-l10n-id="font-language-group-bengali" + /> + <menuitem + value="zh-CN" + data-l10n-id="font-language-group-simpl-chinese" + /> + <menuitem + value="zh-HK" + data-l10n-id="font-language-group-trad-chinese-hk" + /> + <menuitem + value="zh-TW" + data-l10n-id="font-language-group-trad-chinese" + /> + <menuitem + value="x-cyrillic" + data-l10n-id="font-language-group-cyrillic" + /> + <menuitem + value="x-devanagari" + data-l10n-id="font-language-group-devanagari" + /> + <menuitem + value="x-ethi" + data-l10n-id="font-language-group-ethiopic" + /> + <menuitem + value="x-geor" + data-l10n-id="font-language-group-georgian" + /> + <menuitem value="el" data-l10n-id="font-language-group-el" /> + <menuitem + value="x-gujr" + data-l10n-id="font-language-group-gujarati" + /> + <menuitem + value="x-guru" + data-l10n-id="font-language-group-gurmukhi" + /> + <menuitem value="he" data-l10n-id="font-language-group-hebrew" /> + <menuitem + value="ja" + data-l10n-id="font-language-group-japanese" + /> + <menuitem + value="x-knda" + data-l10n-id="font-language-group-kannada" + /> + <menuitem + value="x-khmr" + data-l10n-id="font-language-group-khmer" + /> + <menuitem value="ko" data-l10n-id="font-language-group-korean" /> + <menuitem + value="x-western" + data-l10n-id="font-language-group-latin" + /> + <menuitem + value="x-mlym" + data-l10n-id="font-language-group-malayalam" + /> + <menuitem + value="x-math" + data-l10n-id="font-language-group-math" + /> + <menuitem + value="x-orya" + data-l10n-id="font-language-group-odia" + /> + <menuitem + value="x-sinh" + data-l10n-id="font-language-group-sinhala" + /> + <menuitem + value="x-tamil" + data-l10n-id="font-language-group-tamil" + /> + <menuitem + value="x-telu" + data-l10n-id="font-language-group-telugu" + /> + <menuitem value="th" data-l10n-id="font-language-group-thai" /> + <menuitem + value="x-tibt" + data-l10n-id="font-language-group-tibetan" + /> + <menuitem + value="x-cans" + data-l10n-id="font-language-group-canadian" + /> + <menuitem + value="x-unicode" + data-l10n-id="font-language-group-other" + /> + </menupopup> + </menulist> + </hbox> + <separator /> + <box id="font-chooser-group"> + <!-- proportional row --> + <hbox align="center" pack="end"> + <label data-l10n-id="fonts-proportional-label" /> + </hbox> + <menulist id="defaultFontType"> + <menupopup> + <menuitem value="serif" data-l10n-id="default-font-serif" /> + <menuitem + value="sans-serif" + data-l10n-id="default-font-sans-serif" + /> + </menupopup> + </menulist> + <hbox align="center" pack="end"> + <label + control="sizeVar" + data-l10n-id="font-size-proportional-label" + class="startSpacing" + /> + </hbox> + <menulist id="sizeVar" delayprefsave="true"> + <menupopup> + <menuitem value="9" label="9" /> + <menuitem value="10" label="10" /> + <menuitem value="11" label="11" /> + <menuitem value="12" label="12" /> + <menuitem value="13" label="13" /> + <menuitem value="14" label="14" /> + <menuitem value="15" label="15" /> + <menuitem value="16" label="16" /> + <menuitem value="17" label="17" /> + <menuitem value="18" label="18" /> + <menuitem value="20" label="20" /> + <menuitem value="22" label="22" /> + <menuitem value="24" label="24" /> + <menuitem value="26" label="26" /> + <menuitem value="28" label="28" /> + <menuitem value="30" label="30" /> + <menuitem value="32" label="32" /> + <menuitem value="34" label="34" /> + <menuitem value="36" label="36" /> + <menuitem value="40" label="40" /> + <menuitem value="44" label="44" /> + <menuitem value="48" label="48" /> + <menuitem value="56" label="56" /> + <menuitem value="64" label="64" /> + <menuitem value="72" label="72" /> + </menupopup> + </menulist> + + <!-- serif row --> + <hbox align="center" pack="end"> + <label control="serif" data-l10n-id="font-serif-label" /> + </hbox> + <menulist id="serif" delayprefsave="true" /> + <spacer /> + <spacer /> + + <!-- sans-serif row --> + <hbox align="center" pack="end"> + <label control="sans-serif" data-l10n-id="font-sans-serif-label" /> + </hbox> + <menulist id="sans-serif" delayprefsave="true" /> + <spacer /> + <spacer /> + + <!-- monospace row --> + <hbox align="center" pack="end"> + <label control="monospace" data-l10n-id="font-monospace-label" /> + </hbox> + <menulist id="monospace" crop="end" delayprefsave="true" /> + <hbox align="center" pack="end"> + <label + control="sizeMono" + data-l10n-id="font-size-monospace-label" + class="startSpacing" + /> + </hbox> + <menulist id="sizeMono" delayprefsave="true"> + <menupopup> + <menuitem value="9" label="9" /> + <menuitem value="10" label="10" /> + <menuitem value="11" label="11" /> + <menuitem value="12" label="12" /> + <menuitem value="13" label="13" /> + <menuitem value="14" label="14" /> + <menuitem value="15" label="15" /> + <menuitem value="16" label="16" /> + <menuitem value="17" label="17" /> + <menuitem value="18" label="18" /> + <menuitem value="20" label="20" /> + <menuitem value="22" label="22" /> + <menuitem value="24" label="24" /> + <menuitem value="26" label="26" /> + <menuitem value="28" label="28" /> + <menuitem value="30" label="30" /> + <menuitem value="32" label="32" /> + <menuitem value="34" label="34" /> + <menuitem value="36" label="36" /> + <menuitem value="40" label="40" /> + <menuitem value="44" label="44" /> + <menuitem value="48" label="48" /> + <menuitem value="56" label="56" /> + <menuitem value="64" label="64" /> + <menuitem value="72" label="72" /> + </menupopup> + </menulist> + </box> + + <separator class="thin" /> + + <hbox flex="1"> + <spacer flex="1" /> + <hbox align="center" pack="end"> + <label data-l10n-id="font-min-size-label" control="minSize" /> + <menulist id="minSize"> + <menupopup> + <menuitem value="0" data-l10n-id="min-size-none" /> + <menuitem value="9" label="9" /> + <menuitem value="10" label="10" /> + <menuitem value="11" label="11" /> + <menuitem value="12" label="12" /> + <menuitem value="13" label="13" /> + <menuitem value="14" label="14" /> + <menuitem value="15" label="15" /> + <menuitem value="16" label="16" /> + <menuitem value="17" label="17" /> + <menuitem value="18" label="18" /> + <menuitem value="20" label="20" /> + <menuitem value="22" label="22" /> + <menuitem value="24" label="24" /> + <menuitem value="26" label="26" /> + <menuitem value="28" label="28" /> + <menuitem value="30" label="30" /> + <menuitem value="32" label="32" /> + <menuitem value="34" label="34" /> + <menuitem value="36" label="36" /> + <menuitem value="40" label="40" /> + <menuitem value="44" label="44" /> + <menuitem value="48" label="48" /> + <menuitem value="56" label="56" /> + <menuitem value="64" label="64" /> + <menuitem value="72" label="72" /> + </menupopup> + </menulist> + </hbox> + </hbox> + </html:fieldset> + + <html:fieldset> + <html:legend data-l10n-id="font-control-legend"></html:legend> + <hbox> + <checkbox + id="useDocumentFonts" + data-l10n-id="use-document-fonts-checkbox" + preference="browser.display.use_document_fonts" + /> + </hbox> + <hbox> + <checkbox + id="mailFixedWidthMessages" + data-l10n-id="use-fixed-width-plain-checkbox" + preference="mail.fixed_width_messages" + /> + </hbox> + </html:fieldset> + </dialog> + </html:body> +</html> diff --git a/comm/mail/components/preferences/general.inc.xhtml b/comm/mail/components/preferences/general.inc.xhtml new file mode 100644 index 0000000000..438624649b --- /dev/null +++ b/comm/mail/components/preferences/general.inc.xhtml @@ -0,0 +1,1096 @@ +# 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/. + <script src="chrome://communicator/content/utilityOverlay.js" /> + <script src="chrome://messenger/content/preferences/general.js"/> + <script src="chrome://mozapps/content/preferences/fontbuilder.js"/> + + <commandset id="appPaneCommandSet"> + <command id="cmd_delete" + oncommand="gGeneralPane.onDelete();"/> + </commandset> + + <keyset id="appPaneKeyset"> + <key keycode="VK_BACK" modifiers="any" command="cmd_delete"/> + <key keycode="VK_DELETE" modifiers="any" command="cmd_delete"/> + </keyset> + + <keyset> + <key data-l10n-id="focus-search-shortcut" modifiers="accel" + oncommand="gGeneralPane.focusFilterBox();"/> + <key data-l10n-id="focus-search-shortcut-alt" modifiers="accel" + oncommand="gGeneralPane.focusFilterBox();"/> + </keyset> + + <stringbundle id="bundlePreferences" src="chrome://messenger/locale/preferences/preferences.properties"/> +#ifdef HAVE_SHELL_SERVICE + <stringbundle id="bundleBrand" src="chrome://branding/locale/brand.properties"/> +#endif + <html:template id="paneGeneral"> + <hbox id="generalCategory" + class="subcategory" + data-category="paneGeneral"> + <html:h1 data-l10n-id="pane-general-title"/> + </hbox> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <html:legend data-l10n-id="general-legend"></html:legend> + <vbox> + <hbox align="start"> + <checkbox id="mailnewsStartPageEnabled" + preference="mailnews.start_page.enabled" + data-l10n-id="start-page-label"/> + </hbox> + <hbox align="center" class="input-container"> + <label data-l10n-id="location-label" control="mailnewsStartPageUrl"/> + <html:input id="mailnewsStartPageUrl" + type="url" + preference="mailnews.start_page.url"/> + <button is="highlightable-button" id="browseForStartPageUrl" + data-l10n-id="restore-default-label" + oncommand="gGeneralPane.restoreDefaultStartPage();"> + </button> + </hbox> + </vbox> + </html:fieldset> + </html:div> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <html:legend data-l10n-id="default-search-engine"></html:legend> + <hbox align="center"> + <hbox> + <menulist id="defaultWebSearch"> + <menupopup id="defaultWebSearchPopup"/> + </menulist> + </hbox> + <button is="highlightable-button" id="addSearchEngine" + data-l10n-id="add-web-search-engine" + oncommand="gGeneralPane.addSearchEngine();"/> + <button is="highlightable-button" id="removeSearchEngine" + data-l10n-id="remove-search-engine" + oncommand="gGeneralPane.removeSearchEngine();"/> + </hbox> + </html:fieldset> + </html:div> + +#ifdef HAVE_SHELL_SERVICE + <html:div data-category="paneGeneral"> + <html:fieldset id="systemDefaultsGroup" data-category="paneGeneral"> + <html:legend data-l10n-id="system-integration-legend"></html:legend> + <vbox> + <hbox id="checkDefaultBox" align="center"> + <checkbox id="alwaysCheckDefault" + preference="mail.shell.checkDefaultClient" + data-l10n-id="always-check-default"/> + <spacer flex="1"/> + <hbox> + <button is="highlightable-button" id="checkDefaultButton" + data-l10n-id="check-default-button" + oncommand="gGeneralPane.checkDefaultNow();" + preference="pref.general.disable_button.default_mail" + search-l10n-ids=" + system-integration-title.title, + system-integration-dialog.buttonlabelaccept, + system-integration-dialog.buttonlabelcancel, + system-integration-dialog.buttonlabelcancel2, + default-client-intro, + unset-default-tooltip, + checkbox-email-label.label, + checkbox-newsgroups-label.label, + checkbox-feeds-label.label, + system-search-integration-label.label, + check-on-startup-label.label"/> + </hbox> + </hbox> +#ifdef XP_WIN + <hbox align="start"> + <checkbox data-l10n-id="minimize-to-tray-label" + preference="mail.minimizeToTray"/> + </hbox> +#endif + <hbox id="searchIntegrationContainer"> + <checkbox id="searchIntegration" + preference="searchintegration.enable" + data-l10n-id="search-integration-label"/> + </hbox> + </vbox> + </html:fieldset> + </html:div> +#endif + + <hbox id="languageAndAppearanceCategory" + class="subcategory" + data-category="paneGeneral"> + <html:h1 data-l10n-id="general-language-and-appearance-header"/> + </hbox> + + <!-- Window layout --> + <html:div data-category="paneGeneral"> + <html:fieldset id="layoutGroup" data-category="paneGeneral"> + <html:legend data-l10n-id="window-layout-legend"></html:legend> + <hbox> + <checkbox id="drawInTitlebar" + data-l10n-id="draw-in-titlebar-label" + preference="mail.tabs.drawInTitlebar"/> + <spacer flex="1"/> + </hbox> + <hbox> + <vbox> + <checkbox id="autoHideTabbar" + data-l10n-id="auto-hide-tabbar-label" + preference="mail.tabs.autoHide"/> + <description data-l10n-id="auto-hide-tabbar-description" + class="tip-caption indent"/> + </vbox> + <spacer flex="1"/> + </hbox> + </html:fieldset> + </html:div> + + <!-- Fonts and Colors --> + <html:div data-category="paneGeneral"> + <html:fieldset id="fontsGroup" data-category="paneGeneral"> + <html:legend data-l10n-id="fonts-legend"></html:legend> + + <hbox id="fontSettings" flex="1"> + <vbox id="fontRow" flex="1"> + <hbox align="center"> + <label data-l10n-id="default-font-label" control="defaultFont"/> + <hbox flex="1"> + <menulist id="defaultFont" flex="1" sizetopopup="pref" crop="center"> + <menupopup crop="center"/> + </menulist> + </hbox> + <label data-l10n-id="default-size-label" control="defaultFontSize"/> + <hbox flex="1"> + <menulist id="defaultFontSize" flex="1"> + <menupopup crop="center"> + <menuitem value="9" label="9"/> + <menuitem value="10" label="10"/> + <menuitem value="11" label="11"/> + <menuitem value="12" label="12"/> + <menuitem value="13" label="13"/> + <menuitem value="14" label="14"/> + <menuitem value="15" label="15"/> + <menuitem value="16" label="16"/> + <menuitem value="17" label="17"/> + <menuitem value="18" label="18"/> + <menuitem value="20" label="20"/> + <menuitem value="22" label="22"/> + <menuitem value="24" label="24"/> + <menuitem value="26" label="26"/> + <menuitem value="28" label="28"/> + <menuitem value="30" label="30"/> + <menuitem value="32" label="32"/> + <menuitem value="34" label="34"/> + <menuitem value="36" label="36"/> + <menuitem value="40" label="40"/> + <menuitem value="44" label="44"/> + <menuitem value="48" label="48"/> + <menuitem value="56" label="56"/> + <menuitem value="64" label="64"/> + <menuitem value="72" label="72"/> + </menupopup> + </menulist> + </hbox> + </hbox> + </vbox> + <vbox id="colorsRow"> + <hbox flex="1"> + <button is="highlightable-button" id="advancedFonts" + data-l10n-id="font-options-button" + oncommand="gGeneralPane.configureFonts();" + flex="1" + search-l10n-ids=" + fonts-label-default-unnamed.label, + fonts-dialog-title, + fonts-language-legend.value, + fonts-proportional-label.value, + font-language-group-latin.label, + font-language-group-japanese.label, + font-language-group-trad-chinese.label, + font-language-group-simpl-chinese.label, + font-language-group-trad-chinese-hk.label, + font-language-group-korean.label, + font-language-group-cyrillic.label, + font-language-group-el.label, + font-language-group-other.label, + font-language-group-thai.label, + font-language-group-hebrew.label, + font-language-group-arabic.label, + font-language-group-devanagari.label, + font-language-group-tamil.label, + font-language-group-armenian.label, + font-language-group-bengali.label, + font-language-group-canadian.label, + font-language-group-ethiopic.label, + font-language-group-georgian.label, + font-language-group-gujarati.label, + font-language-group-gurmukhi.label, + font-language-group-khmer.label, + font-language-group-malayalam.label, + font-language-group-math.label, + font-language-group-odia.label, + font-language-group-telugu.label, + font-language-group-kannada.label, + font-language-group-sinhala.label, + font-language-group-tibetan.label, + default-font-serif.label, + default-font-sans-serif.label, + font-size-label.value, + font-size-monospace-label.value, + font-serif-label.value, + font-sans-serif-label.value, + font-monospace-label.value, + font-min-size-label.value, + min-size-none.label, + font-control-legend, + use-document-fonts-checkbox.label, + use-fixed-width-plain-checkbox.label, + text-encoding-legend, + text-encoding-description, + font-outgoing-email-label.value, + font-incoming-email-label.value, + default-font-reply-checkbox.label"/> + </hbox> + <hbox flex="1"> + <button is="highlightable-button" id="colors" + data-l10n-id="color-options-button" + oncommand="gGeneralPane.configureColors();" + flex="1" + search-l10n-ids=" + colors-dialog-window2.title, + colors-dialog-legend, + text-color-label.value, + background-color-label.value, + use-system-colors.label, + colors-link-legend, + link-color-label.value, + visited-link-color-label.value, + underline-link-checkbox.label, + override-color-label.value, + override-color-always.label, + override-color-auto.label, + override-color-never.label"/> + </hbox> + </vbox> + </hbox> + <hbox> + <html:legend data-l10n-id="display-width-legend"></html:legend> + </hbox> + <hbox> + <checkbox id="displayGlyph" + preference="mail.display_glyph" + data-l10n-id="convert-emoticons-label"/> + <spacer flex="1"/> + </hbox> + + <separator class="thin"/> + + <label control="displayText" data-l10n-id="display-text-label"/> + <hbox id="displayText" class="indent" align="center" role="group"> + <label data-l10n-id="style-label" control="mailQuotedStyle"/> + <hbox> + <menulist id="mailQuotedStyle" preference="mail.quoted_style"> + <menupopup> + <menuitem value="0" data-l10n-id="regular-style-item"/> + <menuitem value="1" data-l10n-id="bold-style-item"/> + <menuitem value="2" data-l10n-id="italic-style-item"/> + <menuitem value="3" data-l10n-id="bold-italic-style-item"/> + </menupopup> + </menulist> + </hbox> + <label data-l10n-id="size-label" control="mailQuotedSize"/> + <hbox> + <menulist id="mailQuotedSize" preference="mail.quoted_size"> + <menupopup> + <menuitem value="0" data-l10n-id="regular-size-item"/> + <menuitem value="1" data-l10n-id="bigger-size-item"/> + <menuitem value="2" data-l10n-id="smaller-size-item"/> + </menupopup> + </menulist> + </hbox> + <label data-l10n-id="quoted-text-color" control="citationmenu"/> + <html:input type="color" id="citationmenu" preference="mail.citation_color"/> + </hbox> + </html:fieldset> + </html:div> + + <!-- Date and time formatting --> + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <html:legend data-l10n-id="datetime-formatting-legend"></html:legend> + <radiogroup id="formatLocale" align="start" + preference="intl.regional_prefs.use_os_locales" + orient="vertical"> + <radio id="appLocale" + value="false"/> + <!-- label and accesskey will be set dynamically --> + <radio id="rsLocale" + value="true"/> + <!-- label and accesskey will be set dynamically --> + </radiogroup> + </html:fieldset> + </html:div> + + <html:div data-category="paneGeneral"> + <html:fieldset id="messengerLanguagesBox" data-category="paneGeneral" hidden="hidden"> + <html:legend data-l10n-id="language-selector-legend"></html:legend> + <vbox align="start"> + <description flex="1" + controls="chooseMessengerLanguage" + data-l10n-id="choose-messenger-language-description"/> + <hbox> + <hbox> + <menulist id="primaryMessengerLocale" + oncommand="gGeneralPane.onPrimaryMessengerLanguageMenuChange(event)"> + <menupopup/> + </menulist> + </hbox> + <hbox> + <button is="highlightable-button" id="manageMessengerLanguagesButton" + class="accessory-button" + data-l10n-id="manage-messenger-languages-button" + oncommand="gGeneralPane.showMessengerLanguagesSubDialog({search: false})" + search-l10n-ids=" + languages-customize-moveup.label, + languages-customize-movedown.label, + languages-customize-remove.label, + languages-customize-select-language.placeholder, + languages-customize-add.label, + messenger-languages-window2.title, + messenger-languages-description, + messenger-languages-search, + messenger-languages-searching.label, + messenger-languages-downloading.label, + messenger-languages-select-language.label, + messenger-languages-installed-label, + messenger-languages-available-label, + messenger-languages-error"/> + </hbox> + </hbox> + </vbox> + <hbox id="confirmMessengerLanguage" + class="message-bar" + align="center" + hidden="true"> + <html:img class="message-bar-icon" + src="chrome://global/skin/icons/info.svg" alt="" /> + <vbox class="message-bar-content-container" align="stretch" flex="1"/> + </hbox> + </html:fieldset> + </html:div> + + <!-- Scrolling --> + <html:div data-category="paneGeneral"> + <html:fieldset id="scrollingGroup" data-category="paneGeneral"> + <html:legend data-l10n-id="scrolling-legend"></html:legend> + <hbox> + <checkbox id="useAutoScroll" + data-l10n-id="autoscroll-label" + preference="general.autoScroll"/> + <spacer flex="1"/> + </hbox> + <hbox> + <checkbox id="useSmoothScrolling" + data-l10n-id="smooth-scrolling-label" + preference="general.smoothScroll"/> + <spacer flex="1"/> + </hbox> +#ifdef MOZ_WIDGET_GTK + <hbox> + <checkbox id="useOverlayScrollbars" + data-l10n-id="browsing-gtk-use-non-overlay-scrollbars" + preference="widget.gtk.overlay-scrollbars.enabled"/> + <spacer flex="1"/> + </hbox> +#endif + </html:fieldset> + </html:div> + + <hbox id="incomingMailCategory" + class="subcategory" + data-category="paneGeneral"> + <html:h1 data-l10n-id="general-incoming-mail-header"/> + </hbox> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <html:legend data-l10n-id="new-message-arrival"></html:legend> +#if defined(XP_MACOSX) || defined(XP_WIN) + <hbox align="center"> + <description flex="1" data-l10n-id="change-dock-icon"/> + <hbox> + <button is="highlightable-button" id="dockOptions" + oncommand="gGeneralPane.configureDockOptions();" + data-l10n-id="app-icon-options" + search-l10n-ids=" + dock-options-window-dialog2.title, + bounce-system-dock-icon.label, + dock-icon-legend, + dock-icon-show-label.value, + count-unread-messages-radio.label, + count-new-messages-radio.label, + notification-settings-info2"/> + </hbox> + </hbox> +#endif +#ifdef XP_MACOSX + <description class="bold" data-l10n-id="notification-settings2"/> +#else + <hbox align="center"> + <checkbox id="newMailNotificationAlert" + data-l10n-id="animated-alert-label" + preference="mail.biff.show_alert"/> + <spacer flex="1"/> + <hbox> + <button is="highlightable-button" id="customizeMailAlert" + oncommand="gGeneralPane.customizeMailAlert();" + data-l10n-id="customize-alert-label" + search-l10n-ids=" + notifications-dialog-window.title, + customize-alert-description, + preview-text-checkbox.label, + subject-checkbox.label, + sender-checkbox.label, + open-time-label-before.value, + open-time-label-after.value"/> + </hbox> + </hbox> + <hbox align="center" class="indent"> + <checkbox id="useSystemNotificationAlert" + data-l10n-id="biff-use-system-alert" + preference="mail.biff.use_system_alert"/> + </hbox> +#ifdef XP_WIN + <vbox> + <checkbox id="newMailNotificationTrayIcon" + preference="mail.biff.show_tray_icon" + data-l10n-id="tray-icon-unread-label"/> + <description class="indent tip-caption" + flex="1" + data-l10n-id="tray-icon-unread-description"/> + </vbox> +#endif +#endif + + <hbox align="center"> + <checkbox id="newMailNotification" + preference="mail.biff.play_sound" + data-l10n-id="mail-play-sound-label" + oncommand="gGeneralPane.updatePlaySound();"/> + <spacer flex="1"/> + <button is="highlightable-button" id="playSound" + data-l10n-id="mail-play-button" + oncommand="gGeneralPane.previewSound();"/> + </hbox> + +#ifndef XP_MACOSX + <radiogroup id="soundType" + class="indent" + preference="mail.biff.play_sound.type" + orient="vertical" + oncommand="gGeneralPane.updatePlaySound();" + aria-labelledby="newMailNotification"> + <hbox> + <radio id="system" + value="0" + data-l10n-id="mail-system-sound-label"/> + <spacer flex="1"/> + </hbox> + <hbox> + <radio id="custom" + value="1" + data-l10n-id="mail-custom-sound-label"/> + <spacer flex="1"/> + </hbox> + </radiogroup> +#endif + <hbox align="center" class="input-container"> + <html:input id="soundUrlLocation" + type="text" + class="input-filefield indent" + readonly="readonly" + preference="mail.biff.play_sound.url" + preference-editable="true" + aria-labelledby="custom"/> + <button is="highlightable-button" id="browseForSound" + data-l10n-id="mail-browse-sound-button" + oncommand="gGeneralPane.browseForSoundFile();"/> + </hbox> + </html:fieldset> + </html:div> + + <hbox id="filesAttachmentCategory" + class="subcategory" + data-category="paneGeneral"> + <html:h1 data-l10n-id="general-files-and-attachment-header"/> + </hbox> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <search-textbox id="filter" + data-l10n-id="search-handler-table" + data-l10n-attrs="placeholder" + aria-controls="handlersTable" + oncommand="gGeneralPane._rebuildView();"/> + <separator class="thin"/> + <html:div id="handlersView" + preference="pref.downloads.disable_button.edit_actions"> + <html:table id="handlersTable"> + <html:thead> + <html:tr> + <html:th scope="col" + sort-type="type"> + <html:button class="handlerHeaderButton"> + <html:span class="handlerHeader" + data-l10n-id="type-column-header"> + </html:span> + <html:img class="handlerSortHeaderIcon" + alt=""/> + </html:button> + </html:th> + <html:th scope="col" + sort-type="action"> + <html:button class="handlerHeaderButton"> + <html:span class="handlerHeader" + data-l10n-id="action-column-header"> + </html:span> + <html:img class="handlerSortHeaderIcon" + alt=""/> + </html:button> + </html:th> + </html:tr> + </html:thead> + <html:tbody> + </html:tbody> + </html:table> + </html:div> + + <separator class="thin"/> + + <vbox align="start"> + <radiogroup id="saveWhere" flex="1" + preference="browser.download.useDownloadDir"> + <hbox id="saveToRow" align="center" class="input-container"> + <radio id="saveTo" value="true" + data-l10n-id="save-to-label" + aria-labelledby="saveTo downloadFolder"/> + <html:input id="downloadFolder" + class="input-filefield" + type="text" + readonly="readonly" + aria-labelledby="saveTo"/> + <button is="highlightable-button" id="chooseFolder" + oncommand="gDownloadDirSection.chooseFolder();" + data-l10n-id="choose-folder-label"/> + </hbox> + <hbox> + <radio id="alwaysAsk" + value="false" + data-l10n-id="always-ask-label"/> + </hbox> + </radiogroup> + </vbox> + </html:fieldset> + </html:div> + + <hbox id="tagsCategory" + class="subcategory" + data-category="paneGeneral"> + <html:h1 data-l10n-id="general-tags-header"/> + </hbox> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <label control="tagList" data-l10n-id="display-tags-text"/> + <hbox> + <richlistbox id="tagList" + flex="1" + ondblclick="gGeneralPane.editTag();" + onselect="gGeneralPane.onSelectTag();"/> + <vbox id="tagButtons"> + <hbox> + <button is="highlightable-button" id="newTagButton" + data-l10n-id="new-tag-button" + oncommand="gGeneralPane.addTag();" + search-l10n-ids=" + tag-dialog-window.title, + tag-name-label.value"/> + </hbox> + <hbox> + <button is="highlightable-button" id="editTagButton" + disabled="true" + data-l10n-id="edit-tag-button" + oncommand="gGeneralPane.editTag();" + search-l10n-ids=" + tag-dialog-window.title, + tag-name-label.value"/> + </hbox> + <button is="highlightable-button" id="removeTagButton" + disabled="true" + data-l10n-id="delete-tag-button" + oncommand="gGeneralPane.removeTag();"/> + </vbox> + </hbox> + </html:fieldset> + </html:div> + + <hbox id="readingAndDisplayCategory" + class="subcategory" + data-category="paneGeneral"> + <html:h1 data-l10n-id="general-reading-and-display-header"/> + </hbox> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <vbox> + <hbox> + <checkbox id="viewAttachmentsInline" + data-l10n-id="view-attachments-inline" + preference="mail.inline_attachments"/> + </hbox> + + <hbox> + <checkbox id="automaticallyMarkAsRead" + preference="mailnews.mark_message_read.auto" + data-l10n-id="auto-mark-as-read"/> + </hbox> + + <radiogroup id="markAsReadAutoPreferences" orient="vertical" + class="indent" + align="start" + preference="mailnews.mark_message_read.delay"> + <radio id="mark_read_immediately" + data-l10n-id="mark-read-no-delay" + value="false"/> + <hbox align="center"> + <radio id="markAsReadAfterDelay" value="true" + data-l10n-id="mark-read-delay"/> + <html:input id="markAsReadDelay" type="number" class="size3" + min="1" max="2147483" + preference="mailnews.mark_message_read.delay.interval" + aria-labelledby="markAsReadAfterDelay markAsReadDelay secondsLabel"/> + <label id="secondsLabel" data-l10n-id="seconds-label"/> + </hbox> + </radiogroup> + </vbox> + + <separator/> + + <vbox> + <hbox> + <label data-l10n-id="open-msg-label" + control="mailOpenMessageBehavior"/> + </hbox> + <hbox> + <radiogroup id="mailOpenMessageBehavior" class="indent" + preference="mail.openMessageBehavior" + orient="horizontal"> + <radio id="newTab" value="2" data-l10n-id="open-msg-tab"/> + <radio id="newWindow" value="0" data-l10n-id="open-msg-window"/> + <radio id="existingWindow" value="1" + data-l10n-id="open-msg-ex-window"/> + </radiogroup> + </hbox> + <hbox> + <checkbox id="closeMsgOnMoveOrDelete" + data-l10n-id="close-move-delete" + preference="mail.close_message_window.on_delete"/> + </hbox> + </vbox> + + <separator/> + + <hbox> + <label data-l10n-id="display-name-label"/> + </hbox> + <hbox> + <checkbox id="showCondensedAddresses" + data-l10n-id="condensed-addresses-label" + preference="mail.showCondensedAddresses"/> + </hbox> + + <separator class="thin"/> + + <hbox align="center"> + <description flex="1" data-l10n-id="return-receipts-description"/> + <hbox> + <button is="highlightable-button" id="showReturnReceipts" + data-l10n-id="return-receipts-button" + oncommand="gGeneralPane.showReturnReceipts();" + search-l10n-ids=" + receipts-dialog-window.title, + return-receipt-checkbox-control.label, + receipt-arrive-label, + receipt-leave-radio-control.label, + receipt-move-radio-control.label, + receipt-request-label, + receipt-return-never-radio-control.label, + receipt-return-some-radio-control.label, + receipt-not-to-cc-label.value, + receipt-send-never-label.label, + receipt-send-always-label.label, + receipt-send-ask-label.label, + sender-outside-domain-label.value, + other-cases-text-label.value"/> + </hbox> + </hbox> + </html:fieldset> + </html:div> + +#ifdef MOZ_UPDATER + <hbox id="updatesCategory" + class="subcategory" + data-category="paneGeneral"> + <html:h1 data-l10n-id="general-updates-header"/> + </hbox> + + <!-- Update --> + <html:div data-category="paneGeneral"> + <html:fieldset id="updateApp" data-category="paneGeneral"> + <html:legend data-l10n-id="update-app-legend"></html:legend> + <hbox align="center"> + <vbox> + <description> + <label id="version"/> + <label is="text-link" id="releasenotes" hidden="true" data-l10n-id="release-notes-link"></label> + </description> + <description id="distribution" class="text-blurb" hidden="true"/> + <description id="distributionId" class="text-blurb" hidden="true"/> + </vbox> + <spacer flex="1"/> + <vbox> + <hbox> + <button is="highlightable-button" id="showUpdateHistory" + data-l10n-id="update-history-button" + preference="app.update.disable_button.showUpdateHistory" + oncommand="gGeneralPane.showUpdates();" + search-l10n-ids=" + history-title, + history-intro, + close-button-label.buttonlabelcancel, + close-button-label.title, + no-updates-label, + name-header, + date-header, + type-header, + state-header"/> + </hbox> + </vbox> + </hbox> + <hbox id="updateBox"> + <deck id="updateDeck" orient="vertical" flex="1"> + <html:div id="checkForUpdates" class="update-deck-container"> + <html:button id="checkForUpdatesButton" + data-l10n-id="update-check-for-updates-button" + onclick="gAppUpdater.checkForUpdates();"> + </html:button> + </html:div> + <html:div id="downloadAndInstall" class="update-deck-container"> + <html:button id="downloadAndInstallButton" + onclick="gAppUpdater.startDownload();"> + </html:button> + </html:div> + <html:div id="apply" class="update-deck-container"> + <html:button id="updateButton" + data-l10n-id="update-update-button" + onclick="gAppUpdater.buttonRestartAfterDownload();"> + </html:button> + </html:div> + <html:div id="checkingForUpdates" class="update-deck-container"> + <html:img class="update-throbber" alt="" /> + <html:span data-l10n-id="update-checking-for-updates"></html:span> + <!-- Button is only used for presentation in reference to the actual + - button that triggered this action, which would now be + - invisible --> + <html:button data-l10n-id="update-check-for-updates-button" + disabled="true"> + </html:button> + </html:div> + <html:div id="downloading" class="update-deck-container" + data-l10n-id="update-downloading"> + <html:img class="update-throbber" data-l10n-name="icon"></html:img> + <!-- Group within a single span to center align with icon. --> + <html:span id="downloadStatus" data-l10n-name="download-status"></html:span> + </html:div> + <html:div id="applying" class="update-deck-container"> + <html:img class="update-throbber"/> + <html:span data-l10n-id="update-applying"></html:span> + </html:div> + <html:div id="downloadFailed" class="update-deck-container" + data-l10n-id="update-failed"> + <html:a id="failedLink" class="text-link download-link" + data-l10n-name="failed-link"></html:a> + <html:button data-l10n-id="update-check-for-updates-button" + onclick="gAppUpdater.checkForUpdates();"> + </html:button> + </html:div> + <html:div id="policyDisabled" class="update-deck-container"> + <html:span data-l10n-id="update-admin-disabled"></html:span> + </html:div> + <html:div id="noUpdatesFound" class="update-deck-container"> + <html:span data-l10n-id="update-no-updates-found"></html:span> + <html:button data-l10n-id="update-check-for-updates-button" + onclick="gAppUpdater.checkForUpdates();"> + </html:button> + </html:div> + <html:div id="checkingFailed" class="update-deck-container"> + <html:span data-l10n-id="aboutdialog-update-checking-failed"></html:span> + <html:button data-l10n-id="update-check-for-updates-button" + onclick="gAppUpdater.checkForUpdates();"> + </html:button> + </html:div> + <html:div id="otherInstanceHandlingUpdates" + class="update-deck-container"> + <html:span data-l10n-id="update-other-instance-handling-updates"></html:span> + </html:div> + <html:div id="manualUpdate" class="update-deck-container" + data-l10n-id="update-manual"> + <html:a id="manualLink" class="manualLink text-link download-link" + data-l10n-name="manual-link"></html:a> + <html:button data-l10n-id="update-check-for-updates-button" + onclick="gAppUpdater.checkForUpdates();"> + </html:button> + </html:div> + <html:div id="unsupportedSystem" class="update-deck-container" data-l10n-id="update-unsupported"> + <html:a id="unsupportedLink" class="text-link download-link" + data-l10n-name="unsupported-link"></html:a> + </html:div> + <html:div id="restarting" class="update-deck-container"> + <html:span class="update-throbber"></html:span> + <html:span data-l10n-id="update-restarting"></html:span> + </html:div> + <html:div id="internalError" class="update-deck-container" + data-l10n-id="update-internal-error"> + <html:a id="internalErrorLink" class="manualLink text-link download-link" + data-l10n-name="manual-link"></html:a> + <html:button data-l10n-id="update-check-for-updates-button" + onclick="gAppUpdater.checkForUpdates();"> + </html:button> + </html:div> + </deck> + </hbox> + <separator/> + <description id="updateAllowDescription" data-l10n-id="allow-description"/> + <vbox id="updateSettingsContainer"> + <radiogroup id="updateRadioGroup" + align="start"> + <radio id="autoDesktop" + value="true" + data-l10n-id="automatic-updates-label"/> + <radio id="manualDesktop" + value="false" + data-l10n-id="check-updates-label"/> + </radiogroup> + <description id="updateSettingCrossUserWarning" + data-l10n-id="cross-user-udpate-warning" + hidden="true"/> + </vbox> + +#ifdef MOZ_MAINTENANCE_SERVICE + <separator class="thin"/> + <checkbox id="useService" + data-l10n-id="use-service" + preference="app.update.service.enabled"/> +#endif + </html:fieldset> + </html:div> +#endif + + <hbox id="networkAndDiskspaceCategory" + class="subcategory" + data-category="paneGeneral"> + <html:h1 data-l10n-id="general-network-and-diskspace-header"/> + </hbox> + + <!-- Networking & Disk Space --> + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <html:legend data-l10n-id="networking-legend"></html:legend> + <hbox align="center"> + <description control="catProxiesButton" + data-l10n-id="proxy-config-description" + flex="1"/> + <hbox> + <button is="highlightable-button" id="catProxiesButton" + data-l10n-id="network-settings-button" + oncommand="gGeneralPane.showConnections();" + search-l10n-ids=" + connection-dns-over-https-url-resolver, + connection-dns-over-https-url-custom.label, + connection-dns-over-https-custom-label, + connection-proxy-legend, + proxy-type-no.label, + proxy-type-wpad.label, + proxy-type-system.label, + proxy-type-manual.label, + proxy-http-label.value, + http-port-label.value, + proxy-http-sharing.label, + proxy-https-label.value, + ssl-port-label.value, + proxy-socks-label.value, + socks-port-label.value, + proxy-socks4-label.label, + proxy-socks5-label.label, + proxy-type-auto.label, + proxy-reload-label.label, + no-proxy-label.value, + no-proxy-example, + connection-proxy-noproxy-localhost-desc-2, + proxy-password-prompt.label, + proxy-remote-dns.label, + proxy-enable-doh.label"/> + </hbox> + </hbox> + </html:fieldset> + </html:div> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <html:legend data-l10n-id="offline-legend"></html:legend> + <hbox align="center"> + <description data-l10n-id="offline-settings" + control="offlineSettingsButton" + flex="1"/> + <hbox> + <button is="highlightable-button" id="offlineSettingsButton" + data-l10n-id="offline-settings-button" + oncommand="gGeneralPane.showOffline();" + search-l10n-ids=" + offline-dialog-window.title, + autodetect-online-label.label, + startup-label, + status-radio-remember.label, + status-radio-ask.label, + status-radio-always-online.label, + status-radio-always-offline.label, + going-online-label, + going-online-auto.label, + going-online-not.label, + going-online-ask.label, + going-offline-label, + going-offline-auto.label, + going-offline-not.label, + going-offline-ask.label"/> + </hbox> + </hbox> + </html:fieldset> + </html:div> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <html:legend data-l10n-id="diskspace-legend"></html:legend> + <hbox align="center"> + <label id="actualDiskCacheSize" flex="1"/> + <button is="highlightable-button" id="clearCacheButton" + data-l10n-id="clear-cache-button" + oncommand="gGeneralPane.clearCache();"/> + </hbox> + <hbox> + <checkbox id="clearCacheOnShutdown" + preference="privacy.clearOnShutdown.cache" + data-l10n-id="clear-cache-shutdown-label"/> + </hbox> + <hbox> + <checkbox id="allowSmartSize" + preference="browser.cache.disk.smart_size.enabled" + data-l10n-id="smart-cache-label"/> + </hbox> + <hbox align="center" class="indent"> + <label id="useCacheBefore" control="cacheSize" + data-l10n-id="use-cache-before"/> + <html:input id="cacheSize" type="number" class="size4" max="1024" + preference="browser.cache.disk.capacity" + aria-labelledby="useCacheBefore cacheSize useCacheAfter"/> + <label id="useCacheAfter" data-l10n-id="use-cache-after" flex="1"/> + </hbox> + <hbox align="center"> + <checkbox id="offlineCompactFolder" + data-l10n-id="offline-compact-folder" + aria-labelledby="offlineCompactFolder offlineCompactFolderMin compactFolderMB" + preference="mail.prompt_purge_threshhold" + oncommand="gGeneralPane.updateCompactOptions();"/> + <html:input id="offlineCompactFolderMin" type="number" class="size4" + min="1" max="2048" value="200" + preference="mail.purge_threshhold_mb" + aria-labelledby="offlineCompactFolder offlineCompactFolderMin compactFolderMB"/> + <label id="compactFolderMB" data-l10n-id="compact-folder-size" value=""/> + </hbox> + <hbox align="center" class="indent"> + <checkbox id="offlineCompactFolderAutomatically" + data-l10n-id="offline-compact-folder-automatically" + preference="mail.purge.ask"/> + </hbox> + </html:fieldset> + </html:div> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <html:legend data-l10n-id="general-indexing-label"></html:legend> + <vbox> + <hbox> + <checkbox id="enableGloda" + preference="mailnews.database.global.indexer.enabled" + data-l10n-id="enable-gloda-search-label"/> + </hbox> + <hbox align="center"> + <label control="storeTypeMenulist" data-l10n-id="store-type-label"/> + <hbox> + <menulist id="storeTypeMenulist" + oncommand="gGeneralPane.updateDefaultStore(this.selectedItem.value)"> + <menupopup id="storeTypeMenupopup"> + <menuitem id="mboxStore" + data-l10n-id="mbox-store-label" + value="@mozilla.org/msgstore/berkeleystore;1"/> + <menuitem id="maildirStore" + data-l10n-id="maildir-store-label" + value="@mozilla.org/msgstore/maildirstore;1"/> + </menupopup> + </menulist> + </hbox> + </hbox> + <hbox> + <checkbox id="allowHWAccel" + preference="layers.acceleration.disabled" + data-l10n-id="allow-hw-accel"/> + </hbox> + </vbox> + <vbox> + </vbox> + </html:fieldset> + </html:div> + + <separator class="thin" data-category="paneGeneral"/> + + <html:div data-category="paneGeneral"> + <html:fieldset data-category="paneGeneral"> + <hbox pack="end"> + <hbox> + <button is="highlightable-button" id="configEditor" + data-l10n-id="config-editor-button" + oncommand="gGeneralPane.showConfigEdit();" + searchkeywords="about:config" + search-l10n-ids=" + about-config-page-title, + about-config-search-input1.placeholder, + about-config-show-all, + about-config-pref-add-button.title, + about-config-pref-toggle-button.title, + about-config-pref-edit-button.title, + about-config-pref-save-button.title, + about-config-pref-reset-button.title, + about-config-pref-delete-button.title, + about-config-pref-add-type-boolean, + about-config-pref-add-type-number, + about-config-pref-add-type-string + "/> + </hbox> + </hbox> + </html:fieldset> + </html:div> + + </html:template> diff --git a/comm/mail/components/preferences/general.js b/comm/mail/components/preferences/general.js new file mode 100644 index 0000000000..96e8aaad2d --- /dev/null +++ b/comm/mail/components/preferences/general.js @@ -0,0 +1,2962 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +"use strict"; + +/* import-globals-from ../../base/content/aboutDialog-appUpdater.js */ +/* import-globals-from ../../../../toolkit/mozapps/preferences/fontbuilder.js */ +/* import-globals-from preferences.js */ + +// ------------------------------ +// Constants & Enumeration Values + +var { DownloadUtils } = ChromeUtils.importESModule( + "resource://gre/modules/DownloadUtils.sys.mjs" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { UpdateUtils } = ChromeUtils.importESModule( + "resource://gre/modules/UpdateUtils.sys.mjs" +); +var { TagUtils } = ChromeUtils.import("resource:///modules/TagUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetters(this, { + gHandlerService: [ + "@mozilla.org/uriloader/handler-service;1", + "nsIHandlerService", + ], + gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"], +}); + +XPCOMUtils.defineLazyGetter(this, "gIsPackagedApp", () => { + return Services.sysinfo.getProperty("isPackagedApp"); +}); + +const TYPE_PDF = "application/pdf"; + +const PREF_PDFJS_DISABLED = "pdfjs.disabled"; + +const AUTO_UPDATE_CHANGED_TOPIC = "auto-update-config-change"; + +Preferences.addAll([ + { id: "mail.pane_config.dynamic", type: "int" }, + { id: "mailnews.reuse_message_window", type: "bool" }, + { id: "mailnews.start_page.enabled", type: "bool" }, + { id: "mailnews.start_page.url", type: "string" }, + { id: "mail.biff.show_tray_icon", type: "bool" }, + { id: "mail.biff.play_sound", type: "bool" }, + { id: "mail.biff.play_sound.type", type: "int" }, + { id: "mail.biff.play_sound.url", type: "string" }, + { id: "mail.biff.use_system_alert", type: "bool" }, + { id: "general.autoScroll", type: "bool" }, + { id: "general.smoothScroll", type: "bool" }, + { id: "widget.gtk.overlay-scrollbars.enabled", type: "bool", inverted: true }, + { id: "mail.fixed_width_messages", type: "bool" }, + { id: "mail.inline_attachments", type: "bool" }, + { id: "mail.quoted_style", type: "int" }, + { id: "mail.quoted_size", type: "int" }, + { id: "mail.citation_color", type: "string" }, + { id: "mail.display_glyph", type: "bool" }, + { id: "font.language.group", type: "wstring" }, + { id: "intl.regional_prefs.use_os_locales", type: "bool" }, + { id: "mailnews.database.global.indexer.enabled", type: "bool" }, + { id: "mailnews.labels.description.1", type: "wstring" }, + { id: "mailnews.labels.color.1", type: "string" }, + { id: "mailnews.labels.description.2", type: "wstring" }, + { id: "mailnews.labels.color.2", type: "string" }, + { id: "mailnews.labels.description.3", type: "wstring" }, + { id: "mailnews.labels.color.3", type: "string" }, + { id: "mailnews.labels.description.4", type: "wstring" }, + { id: "mailnews.labels.color.4", type: "string" }, + { id: "mailnews.labels.description.5", type: "wstring" }, + { id: "mailnews.labels.color.5", type: "string" }, + { id: "mail.showCondensedAddresses", type: "bool" }, + { id: "mailnews.mark_message_read.auto", type: "bool" }, + { id: "mailnews.mark_message_read.delay", type: "bool" }, + { id: "mailnews.mark_message_read.delay.interval", type: "int" }, + { id: "mail.openMessageBehavior", type: "int" }, + { id: "mail.close_message_window.on_delete", type: "bool" }, + { id: "mail.prompt_purge_threshhold", type: "bool" }, + { id: "mail.purge.ask", type: "bool" }, + { id: "mail.purge_threshhold_mb", type: "int" }, + { id: "browser.cache.disk.capacity", type: "int" }, + { id: "browser.cache.disk.smart_size.enabled", inverted: true, type: "bool" }, + { id: "privacy.clearOnShutdown.cache", type: "bool" }, + { id: "layers.acceleration.disabled", type: "bool", inverted: true }, + { id: "searchintegration.enable", type: "bool" }, + { id: "mail.tabs.drawInTitlebar", type: "bool" }, + { id: "mail.tabs.autoHide", type: "bool" }, +]); +if (AppConstants.platform == "win") { + Preferences.add({ id: "mail.minimizeToTray", type: "bool" }); +} +if (AppConstants.platform != "macosx") { + Preferences.add({ id: "mail.biff.show_alert", type: "bool" }); +} + +var ICON_URL_APP = ""; + +if (AppConstants.MOZ_WIDGET_GTK) { + ICON_URL_APP = "moz-icon://dummy.exe?size=16"; +} else { + ICON_URL_APP = "chrome://messenger/skin/preferences/application.png"; +} + +if (AppConstants.HAVE_SHELL_SERVICE) { + Preferences.addAll([ + { id: "mail.shell.checkDefaultClient", type: "bool" }, + { id: "pref.general.disable_button.default_mail", type: "bool" }, + ]); +} + +if (AppConstants.MOZ_UPDATER) { + Preferences.add({ + id: "app.update.disable_button.showUpdateHistory", + type: "bool", + }); + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + Preferences.add({ id: "app.update.service.enabled", type: "bool" }); + } +} + +var gGeneralPane = { + // The set of types the app knows how to handle. A map of HandlerInfoWrapper + // objects, indexed by type. + _handledTypes: new Map(), + // Map from a handlerInfoWrapper to the corresponding table HandlerRow. + _handlerRows: new Map(), + _handlerMenuId: 0, + + // 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: [], + + // 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. + _visibleDescriptions: new Map(), + + // ----------------------------------- + // Convenience & Performance Shortcuts + + // These get defined by init(). + _brandShortName: null, + _handlerTbody: null, + _filter: null, + _prefsBundle: null, + mPane: null, + mStartPageUrl: "", + mShellServiceWorking: false, + mTagListBox: null, + requestingLocales: null, + + async init() { + function setEventListener(aId, aEventType, aCallback) { + document + .getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gGeneralPane)); + } + + Preferences.addSyncFromPrefListener( + document.getElementById("saveWhere"), + () => gDownloadDirSection.onReadUseDownloadDir() + ); + + this.mPane = document.getElementById("paneGeneral"); + this._prefsBundle = document.getElementById("bundlePreferences"); + this._brandShortName = document + .getElementById("bundleBrand") + .getString("brandShortName"); + this._handlerTbody = document.querySelector("#handlersTable > tbody"); + this._filter = document.getElementById("filter"); + + this._handlerSort = { type: "type", descending: false }; + this._handlerSortHeaders = document.querySelectorAll( + "#handlersTable > thead th[sort-type]" + ); + for (let header of this._handlerSortHeaders) { + let button = header.querySelector("button"); + button.addEventListener( + "click", + this.sort.bind(this, header.getAttribute("sort-type")) + ); + } + + this.updateStartPage(); + this.updatePlaySound( + !Preferences.get("mail.biff.play_sound").value, + Preferences.get("mail.biff.play_sound.url").value, + Preferences.get("mail.biff.play_sound.type").value + ); + if (AppConstants.platform != "macosx") { + this.updateShowAlert(); + } + this.updateWebSearch(); + + // Search integration -- check whether we should hide or disable integration + let hideSearchUI = false; + let disableSearchUI = false; + const { SearchIntegration } = ChromeUtils.import( + "resource:///modules/SearchIntegration.jsm" + ); + if (SearchIntegration) { + if (SearchIntegration.osVersionTooLow) { + hideSearchUI = true; + } else if (SearchIntegration.osComponentsNotRunning) { + disableSearchUI = true; + } + } else { + hideSearchUI = true; + } + + if (hideSearchUI) { + document.getElementById("searchIntegrationContainer").hidden = true; + } else if (disableSearchUI) { + let searchCheckbox = document.getElementById("searchIntegration"); + searchCheckbox.checked = false; + Preferences.get("searchintegration.enable").disabled = true; + } + + // If the shell service is not working, disable the "Check now" button + // and "perform check at startup" checkbox. + try { + Cc["@mozilla.org/mail/shell-service;1"].getService(Ci.nsIShellService); + this.mShellServiceWorking = true; + } catch (ex) { + // The elements may not exist if HAVE_SHELL_SERVICE is off. + if (document.getElementById("alwaysCheckDefault")) { + document.getElementById("alwaysCheckDefault").disabled = true; + document.getElementById("alwaysCheckDefault").checked = false; + } + if (document.getElementById("checkDefaultButton")) { + document.getElementById("checkDefaultButton").disabled = true; + } + this.mShellServiceWorking = false; + } + this._rebuildFonts(); + + var menulist = document.getElementById("defaultFont"); + if (menulist.selectedIndex == -1) { + // Prepend menuitem with empty name and value. + let item = document.createXULElement("menuitem"); + item.setAttribute("label", ""); + item.setAttribute("value", ""); + menulist.menupopup.insertBefore( + item, + menulist.menupopup.firstElementChild + ); + menulist.selectedIndex = 0; + } + + this.formatLocaleSetLabels(); + + if (Services.prefs.getBoolPref("intl.multilingual.enabled")) { + this.initPrimaryMessengerLanguageUI(); + } + + this.mTagListBox = document.getElementById("tagList"); + this.buildTagList(); + this.updateMarkAsReadOptions(); + + document.getElementById("citationmenu").value = Preferences.get( + "mail.citation_color" + ).value; + + // By doing this in a timeout, we let the preferences dialog resize itself + // to an appropriate size before we add a bunch of items to the list. + // Otherwise, if there are many items, and the Applications prefpane + // is the one that gets displayed when the user first opens the dialog, + // the dialog might stretch too much in an attempt to fit them all in. + // XXX Shouldn't we perhaps just set a max-height on the richlistbox? + var _delayedPaneLoad = function (self) { + self._loadAppHandlerData(); + self._rebuildVisibleTypes(); + self._sortVisibleTypes(); + self._rebuildView(); + + // Notify observers that the UI is now ready + Services.obs.notifyObservers(window, "app-handler-pane-loaded"); + }; + this.updateActualCacheSize(); + this.updateCompactOptions(); + + // Default store type initialization. + let storeTypeElement = document.getElementById("storeTypeMenulist"); + // set the menuitem to match the account + let defaultStoreID = Services.prefs.getCharPref( + "mail.serverDefaultStoreContractID" + ); + let targetItem = storeTypeElement.getElementsByAttribute( + "value", + defaultStoreID + ); + storeTypeElement.selectedItem = targetItem[0]; + setTimeout(_delayedPaneLoad, 0, this); + + if (AppConstants.MOZ_UPDATER) { + this.updateReadPrefs(); + gAppUpdater = new appUpdater(); // eslint-disable-line no-global-assign + let updateDisabled = + Services.policies && !Services.policies.isAllowed("appUpdate"); + + if (gIsPackagedApp) { + // When we're running inside an app package, there's no point in + // displaying any update content here, and it would get confusing if we + // did, because our updater is not enabled. + // We can't rely on the hidden attribute for the toplevel elements, + // because of the pane hiding/showing code interfering. + document + .getElementById("updatesCategory") + .setAttribute("style", "display: none !important"); + document + .getElementById("updateApp") + .setAttribute("style", "display: none !important"); + } else if (updateDisabled || UpdateUtils.appUpdateAutoSettingIsLocked()) { + 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", + gGeneralPane.updateWritePrefs + ); + } + + let defaults = Services.prefs.getDefaultBranch(null); + let distroId = defaults.getCharPref("distribution.id", ""); + if (distroId) { + let distroVersion = defaults.getCharPref("distribution.version", ""); + + let distroIdField = document.getElementById("distributionId"); + distroIdField.value = distroId + " - " + distroVersion; + distroIdField.style.display = "block"; + + let distroAbout = defaults.getStringPref("distribution.about", ""); + if (distroAbout) { + let distroField = document.getElementById("distribution"); + distroField.value = distroAbout; + distroField.style.display = "block"; + } + } + + 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; + } + } + + let version = AppConstants.MOZ_APP_VERSION_DISPLAY; + + // Include the build ID and display warning 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://messenger/locale/messenger.properties" + ); + let archResource = Services.appinfo.is64Bit + ? "aboutDialog.architecture.sixtyFourBit" + : "aboutDialog.architecture.thirtyTwoBit"; + let arch = bundle.GetStringFromName(archResource); + version += ` (${arch})`; + + document.l10n.setAttributes( + document.getElementById("version"), + "update-app-version", + { version } + ); + + if (!AppConstants.NIGHTLY_BUILD) { + // 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; + } + } + } + // Initialize Application section. + + // Listen for window unload so we can remove our preference observers. + window.addEventListener("unload", this); + + Services.obs.addObserver(this, AUTO_UPDATE_CHANGED_TOPIC); + Services.prefs.addObserver("mailnews.tags.", this); + } + + Preferences.addSyncFromPrefListener( + document.getElementById("allowSmartSize"), + () => this.readSmartSizeEnabled() + ); + + let element = document.getElementById("cacheSize"); + Preferences.addSyncFromPrefListener(element, () => this.readCacheSize()); + Preferences.addSyncToPrefListener(element, () => this.writeCacheSize()); + Preferences.addSyncFromPrefListener(menulist, () => + this.readFontSelection() + ); + Preferences.addSyncFromPrefListener( + document.getElementById("soundUrlLocation"), + () => this.readSoundLocation() + ); + + if (!Services.policies.isAllowed("about:config")) { + document.getElementById("configEditor").disabled = true; + } + }, + + /** + * Restores the default start page as the user's start page + */ + restoreDefaultStartPage() { + var startPage = Preferences.get("mailnews.start_page.url"); + startPage.value = startPage.defaultValue; + }, + + /** + * Returns a formatted url corresponding to the value of mailnews.start_page.url + * Stores the original value of mailnews.start_page.url + */ + readStartPageUrl() { + var pref = Preferences.get("mailnews.start_page.url"); + this.mStartPageUrl = pref.value; + return Services.urlFormatter.formatURL(this.mStartPageUrl); + }, + + /** + * Returns the value of the mailnews start page url represented by the UI. + * If the url matches the formatted version of our stored value, then + * return the unformatted url. + */ + writeStartPageUrl() { + var startPage = document.getElementById("mailnewsStartPageUrl"); + return Services.urlFormatter.formatURL(this.mStartPageUrl) == + startPage.value + ? this.mStartPageUrl + : startPage.value; + }, + + customizeMailAlert() { + gSubDialog.open( + "chrome://messenger/content/preferences/notifications.xhtml", + { features: "resizable=no" } + ); + }, + + configureDockOptions() { + gSubDialog.open( + "chrome://messenger/content/preferences/dockoptions.xhtml", + { features: "resizable=no" } + ); + }, + + convertURLToLocalFile(aFileURL) { + // convert the file url into a nsIFile + if (aFileURL) { + return Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler) + .getFileFromURLSpec(aFileURL); + } + return null; + }, + + readSoundLocation() { + var soundUrlLocation = document.getElementById("soundUrlLocation"); + soundUrlLocation.value = Preferences.get("mail.biff.play_sound.url").value; + if (soundUrlLocation.value) { + soundUrlLocation.label = this.convertURLToLocalFile( + soundUrlLocation.value + ).leafName; + soundUrlLocation.style.backgroundImage = + "url(moz-icon://" + soundUrlLocation.label + "?size=16)"; + } + }, + + previewSound() { + let sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound); + + let soundLocation; + // soundType radio-group isn't used for macOS so it is not in the XUL file + // for the platform. + soundLocation = + AppConstants.platform == "macosx" || + document.getElementById("soundType").value == 1 + ? document.getElementById("soundUrlLocation").value + : ""; + + if (!soundLocation.includes("file://")) { + // User has not set any custom sound file to be played + sound.playEventSound(Ci.nsISound.EVENT_NEW_MAIL_RECEIVED); + } else { + // User has set a custom audio file to be played along the alert. + sound.play(Services.io.newURI(soundLocation)); + } + }, + + browseForSoundFile() { + var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + + // if we already have a sound file, then use the path for that sound file + // as the initial path in the dialog. + var localFile = this.convertURLToLocalFile( + document.getElementById("soundUrlLocation").value + ); + if (localFile) { + fp.displayDirectory = localFile.parent; + } + + // XXX todo, persist the last sound directory and pass it in + fp.init( + window, + document + .getElementById("bundlePreferences") + .getString("soundFilePickerTitle"), + Ci.nsIFilePicker.modeOpen + ); + fp.appendFilters(Ci.nsIFilePicker.filterAudio); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + + fp.open(rv => { + if (rv != Ci.nsIFilePicker.returnOK || !fp.file) { + return; + } + // convert the nsIFile into a nsIFile url + Preferences.get("mail.biff.play_sound.url").value = fp.fileURL.spec; + this.readSoundLocation(); // XXX We shouldn't have to be doing this by hand + this.updatePlaySound(); + }); + }, + + updatePlaySound(soundsDisabled, soundUrlLocation, soundType) { + // Update the sound type radio buttons based on the state of the + // play sound checkbox. + if (soundsDisabled === undefined) { + soundsDisabled = !document.getElementById("newMailNotification").checked; + soundUrlLocation = document.getElementById("soundUrlLocation").value; + } + + // The UI is different on OS X as the user can only choose between letting + // the system play a default sound or setting a custom one. Therefore, + // "soundTypeEl" does not exist on OS X. + if (AppConstants.platform != "macosx") { + var soundTypeEl = document.getElementById("soundType"); + if (soundType === undefined) { + soundType = soundTypeEl.value; + } + + soundTypeEl.disabled = soundsDisabled; + document.getElementById("soundUrlLocation").disabled = + soundsDisabled || soundType != 1; + document.getElementById("browseForSound").disabled = + soundsDisabled || soundType != 1; + document.getElementById("playSound").disabled = + soundsDisabled || (!soundUrlLocation && soundType != 0); + } else { + // On OS X, if there is no selected custom sound then default one will + // be played. We keep consistency by disabling the "Play sound" checkbox + // if the user hasn't selected a custom sound file yet. + document.getElementById("newMailNotification").disabled = + !soundUrlLocation; + document.getElementById("playSound").disabled = !soundUrlLocation; + // The sound type radiogroup is hidden, but we have to keep the + // play_sound.type pref set appropriately. + Preferences.get("mail.biff.play_sound.type").value = + !soundsDisabled && soundUrlLocation ? 1 : 0; + } + }, + + updateStartPage() { + document.getElementById("mailnewsStartPageUrl").disabled = !Preferences.get( + "mailnews.start_page.enabled" + ).value; + document.getElementById("browseForStartPageUrl").disabled = + !Preferences.get("mailnews.start_page.enabled").value; + }, + + updateShowAlert() { + // The button does not exist on all platforms. + let customizeAlertButton = document.getElementById("customizeMailAlert"); + if (customizeAlertButton) { + customizeAlertButton.disabled = !Preferences.get("mail.biff.show_alert") + .value; + } + // The checkmark does not exist on all platforms. + let systemNotification = document.getElementById( + "useSystemNotificationAlert" + ); + if (systemNotification) { + systemNotification.disabled = !Preferences.get("mail.biff.show_alert") + .value; + } + }, + + updateWebSearch() { + let self = this; + Services.search.init().then(async () => { + let defaultEngine = await Services.search.getDefault(); + let engineList = document.getElementById("defaultWebSearch"); + for (let engine of await Services.search.getVisibleEngines()) { + let item = engineList.appendItem(engine.name); + item.engine = engine; + item.className = "menuitem-iconic"; + item.setAttribute( + "image", + engine.iconURI + ? engine.iconURI.spec + : "resource://gre-resources/broken-image.png" + ); + if (engine == defaultEngine) { + engineList.selectedItem = item; + } + } + self.defaultEngines = await Services.search.getAppProvidedEngines(); + self.updateRemoveButton(); + + engineList.addEventListener("command", async () => { + await Services.search.setDefault( + engineList.selectedItem.engine, + Ci.nsISearchService.CHANGE_REASON_USER + ); + self.updateRemoveButton(); + }); + }); + }, + + // Caches the default engines so we only retrieve them once. + defaultEngines: null, + + async updateRemoveButton() { + let engineList = document.getElementById("defaultWebSearch"); + let removeButton = document.getElementById("removeSearchEngine"); + if (this.defaultEngines.includes(await Services.search.getDefault())) { + // Don't allow deletion of a default engine (saves us having a 'restore' button). + removeButton.disabled = true; + } else { + // Don't allow removal of last engine. This shouldn't happen since there should + // always be default engines. + removeButton.disabled = engineList.itemCount <= 1; + } + }, + + /** + * Look up OpenSearch Description URL. + * + * @param url - the url to use as basis for discovery + */ + async lookupOpenSearch(url) { + let response = await fetch(url); + if (!response.ok) { + throw new Error(`Bad response for url=${url}`); + } + let contentType = response.headers.get("Content-Type")?.toLowerCase(); + if ( + contentType == "application/opensearchdescription+xml" || + contentType == "application/xml" || + contentType == "text/xml" + ) { + return url; + } + let doc = new DOMParser().parseFromString( + await response.text(), + "text/html" + ); + let auto = doc.querySelector( + "link[rel='search'][type='application/opensearchdescription+xml']" + ); + if (!auto) { + throw new Error(`No provider discovered for url=${url}`); + } + return /^https?:/.test(auto.href) + ? auto.href + : new URL(url).origin + auto.href; + }, + + async addSearchEngine() { + let input = { value: "https://" }; + let [title, text] = await document.l10n.formatValues([ + "add-opensearch-provider-title", + "add-opensearch-provider-text", + ]); + let result = Services.prompt.prompt(window, title, text, input, null, { + value: false, + }); + input.value = input.value.trim(); + if (!result || !input.value || input.value == "https://") { + return; + } + let url = input.value; + let engine; + try { + url = await this.lookupOpenSearch(url); + engine = await Services.search.addOpenSearchEngine(url, null); + } catch (reason) { + let [title, text] = await document.l10n.formatValues([ + { id: "adding-opensearch-provider-failed-title" }, + { id: "adding-opensearch-provider-failed-text", args: { url } }, + ]); + Services.prompt.alert(window, title, text); + return; + } + // Wait a bit, so the engine iconURI has time to be fetched. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + + // Add new engine to the list, make the added engine the default. + let engineList = document.getElementById("defaultWebSearch"); + let item = engineList.appendItem(engine.name); + item.engine = engine; + item.className = "menuitem-iconic"; + item.setAttribute( + "image", + engine.iconURI + ? engine.iconURI.spec + : "resource://gre-resources/broken-image.png" + ); + engineList.selectedIndex = + engineList.firstElementChild.childElementCount - 1; + await Services.search.setDefault( + engineList.selectedItem.engine, + Ci.nsISearchService.CHANGE_REASON_USER + ); + this.updateRemoveButton(); + }, + + async removeSearchEngine() { + // Deletes the current engine. Firefox does a better job since it + // shows all the engines in the list. But better than nothing. + let defaultEngine = await Services.search.getDefault(); + let engineList = document.getElementById("defaultWebSearch"); + for (let i = 0; i < engineList.itemCount; i++) { + let item = engineList.getItemAtIndex(i); + if (item.engine == defaultEngine) { + await Services.search.removeEngine(item.engine); + item.remove(); + engineList.selectedIndex = 0; + await Services.search.setDefault( + engineList.selectedItem.engine, + Ci.nsISearchService.CHANGE_REASON_USER + ); + this.updateRemoveButton(); + break; + } + } + }, + + /** + * Checks whether Thunderbird is currently registered with the operating + * system as the default app for mail, rss and news. If Thunderbird is not + * currently the default app, the user is given the option of making it the + * default for each type; otherwise, the user is informed that Thunderbird is + * already the default. + */ + checkDefaultNow(aAppType) { + if (!this.mShellServiceWorking) { + return; + } + + // otherwise, bring up the default client dialog + gSubDialog.open( + "chrome://messenger/content/systemIntegrationDialog.xhtml", + { features: "resizable=no" }, + "calledFromPrefs" + ); + }, + + // FONTS + + /** + * Populates the default font list in UI. + */ + _rebuildFonts() { + var langGroupPref = Preferences.get("font.language.group"); + var isSerif = + gGeneralPane._readDefaultFontTypeForLanguage(langGroupPref.value) == + "serif"; + gGeneralPane._selectDefaultLanguageGroup(langGroupPref.value, isSerif); + }, + + /** + * Select the default language group. + */ + _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%"; + + // Make sure font.name-list is created before font.name so that it's + // available at the time readFontSelection below is called. + var prefs = [ + { + format: aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif, + type: "unichar", + element: null, + fonttype: aIsSerif ? "serif" : "sans-serif", + }, + { + format: aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif, + type: "fontname", + element: "defaultFont", + 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) { + preference = Preferences.add({ + id: prefs[i].format.replace(/%LANG%/, aLanguageGroup), + type: prefs[i].type, + }); + } + + if (!prefs[i].element) { + continue; + } + + var element = document.getElementById(prefs[i].element); + if (element) { + if (prefs[i].fonttype) { + await FontBuilder.buildFontList( + aLanguageGroup, + prefs[i].fonttype, + element + ); + } + + element.setAttribute("preference", preference.id); + + preference.setElementValue(element); + } + } + })().catch(console.error); + }, + + /** + * Displays the fonts dialog, where web page font names and sizes can be + * configured. + */ + configureFonts() { + gSubDialog.open("chrome://messenger/content/preferences/fonts.xhtml", { + features: "resizable=no", + }); + }, + + /** + * Displays the colors dialog, where default web page/link/etc. colors can be + * configured. + */ + configureColors() { + gSubDialog.open("chrome://messenger/content/preferences/colors.xhtml", { + features: "resizable=no", + }); + }, + + /** + * 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) { + Preferences.add({ + id: defaultFontTypePref, + type: "string", + name: defaultFontTypePref, + }).on("change", gGeneralPane._rebuildFonts); + } + + // We should return preference.value here, but we can't wait for the binding to load, + // or things get really messy. Fortunately this will give the same answer. + return Services.prefs.getCharPref(defaultFontTypePref); + }, + + /** + * Determine the appropriate value to select for defaultFont, for the + * following cases: + * - there is no setting + * - the font selected by the user is no longer present (e.g. deleted from + * fonts folder) + */ + readFontSelection() { + let element = document.getElementById("defaultFont"); + let preference = Preferences.get(element.getAttribute("preference")); + if (preference.value) { + let fontItem = element.querySelector( + '[value="' + preference.value + '"]' + ); + + // There is a setting that actually is in the list. Respect it. + if (fontItem) { + return undefined; + } + } + + let defaultValue = + element.firstElementChild.firstElementChild.getAttribute("value"); + let languagePref = Preferences.get("font.language.group"); + let defaultType = this._readDefaultFontTypeForLanguage(languagePref.value); + let listPref = Preferences.get( + "font.name-list." + defaultType + "." + languagePref.value + ); + if (!listPref) { + return defaultValue; + } + + let fontNames = listPref.value.split(","); + + for (let fontName of fontNames) { + let fontItem = element.querySelector('[value="' + fontName.trim() + '"]'); + if (fontItem) { + return fontItem.getAttribute("value"); + } + } + return defaultValue; + }, + + async formatLocaleSetLabels() { + // HACK: calling getLocaleDisplayNames may fail the first time due to + // synchronous loading of the .ftl files. If we load the files and wait + // for a known value asynchronously, no such failure will happen. + await new Localization([ + "toolkit/intl/languageNames.ftl", + "toolkit/intl/regionNames.ftl", + ]).formatValue("language-name-en"); + + const osprefs = Cc["@mozilla.org/intl/ospreferences;1"].getService( + Ci.mozIOSPreferences + ); + let appLocale = Services.locale.appLocalesAsBCP47[0]; + let rsLocale = osprefs.regionalPrefsLocales[0]; + let names = Services.intl.getLocaleDisplayNames(undefined, [ + appLocale, + rsLocale, + ]); + let appLocaleRadio = document.getElementById("appLocale"); + let rsLocaleRadio = document.getElementById("rsLocale"); + let appLocaleLabel = this._prefsBundle.getFormattedString( + "appLocale.label", + [names[0]] + ); + let rsLocaleLabel = this._prefsBundle.getFormattedString("rsLocale.label", [ + names[1], + ]); + appLocaleRadio.setAttribute("label", appLocaleLabel); + rsLocaleRadio.setAttribute("label", rsLocaleLabel); + appLocaleRadio.accessKey = this._prefsBundle.getString( + "appLocale.accesskey" + ); + rsLocaleRadio.accessKey = this._prefsBundle.getString("rsLocale.accesskey"); + }, + + // Load the preferences string bundle for other locales with fallbacks. + getBundleForLocales(newLocales) { + let locales = Array.from( + new Set([ + ...newLocales, + ...Services.locale.requestedLocales, + Services.locale.lastFallbackLocale, + ]) + ); + return new Localization( + ["messenger/preferences/preferences.ftl", "branding/brand.ftl"], + false, + undefined, + locales + ); + }, + + initPrimaryMessengerLanguageUI() { + gGeneralPane.updatePrimaryMessengerLanguageUI( + Services.locale.requestedLocale + ); + }, + + /** + * Update the available list of locales and select the locale that the user + * is "selecting". This could be the currently requested locale or a locale + * that the user would like to switch to after confirmation. + * + * @param {string} selected - The selected BCP 47 locale. + */ + async updatePrimaryMessengerLanguageUI(selected) { + // HACK: calling getLocaleDisplayNames may fail the first time due to + // synchronous loading of the .ftl files. If we load the files and wait + // for a known value asynchronously, no such failure will happen. + await new Localization([ + "toolkit/intl/languageNames.ftl", + "toolkit/intl/regionNames.ftl", + ]).formatValue("language-name-en"); + + let available = await getAvailableLocales(); + let localeNames = Services.intl.getLocaleDisplayNames( + undefined, + available, + { preferNative: true } + ); + let locales = available.map((code, i) => ({ code, name: localeNames[i] })); + locales.sort((a, b) => a.name > b.name); + + let fragment = document.createDocumentFragment(); + for (let { code, name } of locales) { + let menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("value", code); + menuitem.setAttribute("label", name); + fragment.appendChild(menuitem); + } + + // Add an option to search for more languages if downloading is supported. + if (Services.prefs.getBoolPref("intl.multilingual.downloadEnabled")) { + let menuitem = document.createXULElement("menuitem"); + menuitem.id = "primaryMessengerLocaleSearch"; + menuitem.setAttribute( + "label", + await document.l10n.formatValue("messenger-languages-search") + ); + menuitem.setAttribute("value", "search"); + menuitem.addEventListener("command", () => { + gGeneralPane.showMessengerLanguagesSubDialog({ search: true }); + }); + fragment.appendChild(menuitem); + } + + let menulist = document.getElementById("primaryMessengerLocale"); + let menupopup = menulist.querySelector("menupopup"); + menupopup.textContent = ""; + menupopup.appendChild(fragment); + menulist.value = selected; + + document.getElementById("messengerLanguagesBox").hidden = false; + }, + + /** + * Open the messenger languages sub dialog in either the normal mode, or search mode. + * The search mode is only available from the menu to change the primary browser + * language. + * + * @param {{ search: boolean }} + */ + showMessengerLanguagesSubDialog({ search }) { + let opts = { + selectedLocalesForRestart: gGeneralPane.selectedLocalesForRestart, + search, + }; + gSubDialog.open( + "chrome://messenger/content/preferences/messengerLanguages.xhtml", + { closingCallback: this.messengerLanguagesClosed }, + opts + ); + }, + + /** + * Returns the assumed script directionality for known Firefox locales. This is + * somewhat crude, but should work until Bug 1750781 lands. + * + * TODO (Bug 1750781) - This should use Intl.LocaleInfo once it is standardized (see + * Bug 1693576), rather than maintaining a hardcoded list of RTL locales. + * + * @param {string} locale + * @returns {"ltr" | "rtl"} + */ + getLocaleDirection(locale) { + if ( + locale == "ar" || + locale == "ckb" || + locale == "fa" || + locale == "he" || + locale == "ur" + ) { + return "rtl"; + } + return "ltr"; + }, + + /** + * Determine the transition strategy for switching the locale based on prefs + * and the switched locales. + * + * @param {Array<string>} newLocales - List of BCP 47 locale identifiers. + * @returns {"locales-match" | "requires-restart" | "live-reload"} + */ + getLanguageSwitchTransitionType(newLocales) { + const { appLocalesAsBCP47 } = Services.locale; + if (appLocalesAsBCP47.join(",") === newLocales.join(",")) { + // The selected locales match, the order matters. + return "locales-match"; + } + + if (Services.prefs.getBoolPref("intl.multilingual.liveReload")) { + if ( + gGeneralPane.getLocaleDirection(newLocales[0]) !== + gGeneralPane.getLocaleDirection(appLocalesAsBCP47[0]) && + !Services.prefs.getBoolPref("intl.multilingual.liveReloadBidirectional") + ) { + // Bug 1750852: The directionality of the text changed, which requires a restart + // until the quality of the switch can be improved. + return "requires-restart"; + } + + return "live-reload"; + } + + return "requires-restart"; + }, + + /* Show or hide the confirm change message bar based on the updated ordering. */ + messengerLanguagesClosed() { + // When the subdialog is closed, settings are stored on gMessengerLanguagesDialog. + // The next time the dialog is opened, a new gMessengerLanguagesDialog is created. + let { selected } = this.gMessengerLanguagesDialog; + + if (!selected) { + // No locales were selected. Cancel the operation. + return; + } + + switch (gGeneralPane.getLanguageSwitchTransitionType(selected)) { + case "requires-restart": + gGeneralPane.showConfirmLanguageChangeMessageBar(selected); + gGeneralPane.updatePrimaryMessengerLanguageUI(selected[0]); + break; + case "live-reload": + Services.locale.requestedLocales = selected; + + gGeneralPane.updatePrimaryMessengerLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gGeneralPane.hideConfirmLanguageChangeMessageBar(); + break; + case "locales-match": + // They matched, so we can reset the UI. + gGeneralPane.updatePrimaryMessengerLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gGeneralPane.hideConfirmLanguageChangeMessageBar(); + break; + default: + throw new Error("Unhandled transition type."); + } + }, + + /* Show the confirmation message bar to allow a restart into the new locales. */ + async showConfirmLanguageChangeMessageBar(locales) { + let messageBar = document.getElementById("confirmMessengerLanguage"); + + // Get the bundle for the new locale. + let newBundle = this.getBundleForLocales(locales); + + // Find the messages and labels. + let messages = await Promise.all( + [newBundle, document.l10n].map(async bundle => + bundle.formatValue("confirm-messenger-language-change-description") + ) + ); + let buttonLabels = await Promise.all( + [newBundle, document.l10n].map(async bundle => + bundle.formatValue("confirm-messenger-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"); + + if (i == 0 && gGeneralPane.getLocaleDirection(locales[0]) === "rtl") { + description.classList.add("rtl-locale"); + } + + description.setAttribute("flex", "1"); + description.textContent = messages[i]; + messageContainer.appendChild(description); + + let button = document.createXULElement("button"); + button.addEventListener("command", gGeneralPane.confirmLanguageChange); + 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; + this.selectedLocalesForRestart = locales; + }, + + hideConfirmLanguageChangeMessageBar() { + let messageBar = document.getElementById("confirmMessengerLanguage"); + messageBar.hidden = true; + let contentContainer = messageBar.querySelector( + ".message-bar-content-container" + ); + contentContainer.textContent = ""; + this.requestingLocales = null; + }, + + /* Confirm the locale change and restart the Thunderbird in the new locale. */ + confirmLanguageChange(event) { + let localesString = (event.target.getAttribute("locales") || "").trim(); + if (!localesString || localesString.length == 0) { + return; + } + let locales = localesString.split(","); + Services.locale.requestedLocales = locales; + + // 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. */ + onPrimaryMessengerLanguageMenuChange(event) { + let locale = event.target.value; + + if (locale == "search") { + return; + } else if (locale == Services.locale.appLocaleAsBCP47) { + this.hideConfirmLanguageChangeMessageBar(); + return; + } + + let newLocales = Array.from( + new Set([locale, ...Services.locale.requestedLocales]).values() + ); + + switch (gGeneralPane.getLanguageSwitchTransitionType(newLocales)) { + case "requires-restart": + // Prepare to change the locales, as they were different. + gGeneralPane.showConfirmLanguageChangeMessageBar(newLocales); + gGeneralPane.updatePrimaryMessengerLanguageUI(newLocales[0]); + break; + case "live-reload": + Services.locale.requestedLocales = newLocales; + gGeneralPane.updatePrimaryMessengerLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gGeneralPane.hideConfirmLanguageChangeMessageBar(); + break; + case "locales-match": + // They matched, so we can reset the UI. + gGeneralPane.updatePrimaryMessengerLanguageUI( + Services.locale.appLocaleAsBCP47 + ); + gGeneralPane.hideConfirmLanguageChangeMessageBar(); + break; + default: + throw new Error("Unhandled transition type."); + } + }, + + // appends the tag to the tag list box + appendTagItem(aTagName, aKey, aColor) { + let item = this.mTagListBox.appendItem(aTagName, aKey); + item.style.color = aColor; + return item; + }, + + buildTagList() { + let tagArray = MailServices.tags.getAllTags(); + for (let i = 0; i < tagArray.length; ++i) { + let taginfo = tagArray[i]; + this.appendTagItem(taginfo.tag, taginfo.key, taginfo.color); + } + }, + + removeTag() { + var index = this.mTagListBox.selectedIndex; + if (index >= 0) { + var itemToRemove = this.mTagListBox.getItemAtIndex(index); + MailServices.tags.deleteKey(itemToRemove.getAttribute("value")); + } + }, + + /** + * Open the edit tag dialog + */ + editTag() { + var index = this.mTagListBox.selectedIndex; + if (index >= 0) { + var tagElToEdit = this.mTagListBox.getItemAtIndex(index); + var args = { + result: "", + keyToEdit: tagElToEdit.getAttribute("value"), + }; + gSubDialog.open( + "chrome://messenger/content/preferences/tagDialog.xhtml", + { features: "resizable=no" }, + args + ); + } + }, + + addTag() { + var args = { result: "", okCallback: addTagCallback }; + gSubDialog.open( + "chrome://messenger/content/preferences/tagDialog.xhtml", + { features: "resizable=no" }, + args + ); + }, + + onSelectTag() { + let btnEdit = document.getElementById("editTagButton"); + let listBox = document.getElementById("tagList"); + + if (listBox.selectedCount > 0) { + btnEdit.disabled = false; + } else { + btnEdit.disabled = true; + } + + document.getElementById("removeTagButton").disabled = btnEdit.disabled; + }, + + /** + * Enable/disable the options of automatic marking as read depending on the + * state of the automatic marking feature. + */ + updateMarkAsReadOptions() { + let enableRadioGroup = Preferences.get( + "mailnews.mark_message_read.auto" + ).value; + let autoMarkAsPref = Preferences.get("mailnews.mark_message_read.delay"); + let autoMarkDisabled = !enableRadioGroup || autoMarkAsPref.locked; + document.getElementById("markAsReadAutoPreferences").disabled = + autoMarkDisabled; + document.getElementById("secondsLabel").disabled = autoMarkDisabled; + gGeneralPane.updateMarkAsReadTextbox(); + }, + + /** + * Automatically enable/disable delay textbox depending on state of the + * Mark As Read On Delay feature. + */ + updateMarkAsReadTextbox() { + let radioGroupEnabled = Preferences.get( + "mailnews.mark_message_read.auto" + ).value; + let textBoxEnabled = Preferences.get( + "mailnews.mark_message_read.delay" + ).value; + let intervalPref = Preferences.get( + "mailnews.mark_message_read.delay.interval" + ); + + let delayTextbox = document.getElementById("markAsReadDelay"); + delayTextbox.disabled = + !radioGroupEnabled || !textBoxEnabled || intervalPref.locked; + if (document.activeElement.id == "markAsReadAutoPreferences") { + delayTextbox.focus(); + } + }, + + /** + * Display the return receipts configuration dialog. + */ + showReturnReceipts() { + gSubDialog.open("chrome://messenger/content/preferences/receipts.xhtml", { + features: "resizable=no", + }); + }, + + /** + * Show the about:config page in a tab. + */ + showConfigEdit() { + // If the about:config tab is already open, switch to the tab. + let mainWin = Services.wm.getMostRecentWindow("mail:3pane"); + let tabmail = mainWin.document.getElementById("tabmail"); + for (let tabInfo of tabmail.tabInfo) { + let tab = tabmail.getTabForBrowser(tabInfo.browser); + if (tab?.urlbar?.value == "about:config") { + tabmail.switchToTab(tabInfo); + return; + } + } + // Wasn't open already. Open in a new tab. + tabmail.openTab("contentTab", { url: "about:config" }); + }, + + /** + * Display the the connection settings dialog. + */ + showConnections() { + gSubDialog.open("chrome://messenger/content/preferences/connection.xhtml"); + }, + + /** + * Display the the offline settings dialog. + */ + showOffline() { + gSubDialog.open("chrome://messenger/content/preferences/offline.xhtml", { + features: "resizable=no", + }); + }, + + /* + * browser.cache.disk.capacity + * - the size of the browser cache in KB + */ + + // Retrieves the amount of space currently used by disk cache + updateActualCacheSize() { + let actualSizeLabel = document.getElementById("actualDiskCacheSize"); + let prefStrBundle = document.getElementById("bundlePreferences"); + + // Needs to root the observer since cache service keeps only a weak reference. + this.observer = { + onNetworkCacheDiskConsumption(consumption) { + let size = DownloadUtils.convertByteUnits(consumption); + // The XBL binding for the string bundle may have been destroyed if + // the page was closed before this callback was executed. + if (!prefStrBundle.getFormattedString) { + return; + } + actualSizeLabel.value = prefStrBundle.getFormattedString( + "actualDiskCacheSize", + size + ); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsICacheStorageConsumptionObserver", + "nsISupportsWeakReference", + ]), + }; + + actualSizeLabel.value = prefStrBundle.getString( + "actualDiskCacheSizeCalculated" + ); + + try { + Services.cache2.asyncGetDiskConsumption(this.observer); + } catch (e) {} + }, + + updateCacheSizeUI(smartSizeEnabled) { + document.getElementById("useCacheBefore").disabled = smartSizeEnabled; + document.getElementById("cacheSize").disabled = smartSizeEnabled; + document.getElementById("useCacheAfter").disabled = smartSizeEnabled; + }, + + readSmartSizeEnabled() { + // The smart_size.enabled preference element is inverted="true", so its + // value is the opposite of the actual pref value + var disabled = Preferences.get( + "browser.cache.disk.smart_size.enabled" + ).value; + this.updateCacheSizeUI(!disabled); + }, + + /** + * Converts the cache size from units of KB to units of MB and returns that + * value. + */ + readCacheSize() { + var preference = Preferences.get("browser.cache.disk.capacity"); + return preference.value / 1024; + }, + + /** + * Converts the cache size as specified in UI (in MB) to KB and returns that + * value. + */ + writeCacheSize() { + var cacheSize = document.getElementById("cacheSize"); + var intValue = parseInt(cacheSize.value, 10); + return isNaN(intValue) ? 0 : intValue * 1024; + }, + + /** + * Clears the cache. + */ + clearCache() { + try { + Services.cache2.clear(); + } catch (ex) {} + this.updateActualCacheSize(); + }, + + updateCompactOptions() { + let disabled = + !Preferences.get("mail.prompt_purge_threshhold").value || + Preferences.get("mail.purge_threshhold_mb").locked; + + document.getElementById("offlineCompactFolderMin").disabled = disabled; + document.getElementById("offlineCompactFolderAutomatically").disabled = + disabled; + }, + + /** + * Set the default store contract ID. + */ + updateDefaultStore(storeID) { + Services.prefs.setCharPref("mail.serverDefaultStoreContractID", storeID); + }, + + /** + * When the user toggles the layers.acceleration.disabled pref, + * sync its new value to the gfx.direct2d.disabled pref too. + * Note that layers.acceleration.disabled is inverted. + */ + updateHardwareAcceleration() { + if (AppConstants.platform == "win") { + let preference = Preferences.get("layers.acceleration.disabled"); + Services.prefs.setBoolPref("gfx.direct2d.disabled", !preference.value); + } + }, + + /** + * Selects the correct item in the update radio group + */ + async updateReadPrefs() { + if ( + AppConstants.MOZ_UPDATER && + (!Services.policies || Services.policies.isAllowed("appUpdate")) && + !gIsPackagedApp + ) { + let radiogroup = document.getElementById("updateRadioGroup"); + radiogroup.disabled = true; + try { + let enabled = await UpdateUtils.getAppUpdateAutoEnabled(); + radiogroup.value = enabled; + radiogroup.disabled = false; + } catch (error) { + console.error(error); + } + } + }, + + /** + * Writes the value of the update radio group to the disk + */ + async updateWritePrefs() { + if ( + AppConstants.MOZ_UPDATER && + (!Services.policies || Services.policies.isAllowed("appUpdate")) && + !gIsPackagedApp + ) { + let radiogroup = document.getElementById("updateRadioGroup"); + let updateAutoValue = radiogroup.value == "true"; + radiogroup.disabled = true; + try { + await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue); + radiogroup.disabled = false; + } catch (error) { + console.error(error); + await this.updateReadPrefs(); + await this.reportUpdatePrefWriteError(); + 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() { + let [title, message] = await document.l10n.formatValues([ + { id: "update-setting-write-failure-title" }, + { + id: "update-setting-write-failure-message", + args: { path: UpdateUtils.configFilePath }, + }, + ]); + + // Set up the Ok Button + let buttonFlags = + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK; + Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + null, + null, + null, + null, + {} + ); + }, + + async checkUpdateInProgress() { + let um = Cc["@mozilla.org/updates/update-manager;1"].getService( + Ci.nsIUpdateManager + ); + if (!um.readyUpdate && !um.downloadingUpdate) { + return; + } + + let [title, message, okButton, cancelButton] = + await document.l10n.formatValues([ + { id: "update-in-progress-title" }, + { id: "update-in-progress-message" }, + { id: "update-in-progress-ok-button" }, + { id: "update-in-progress-cancel-button" }, + ]); + + // Continue is the cancel button which is BUTTON_POS_1 and is set as the + // default so pressing escape or using a platform standard method of closing + // the UI will not discard the update. + let buttonFlags = + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + + Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 + + Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; + + let rv = Services.prompt.confirmEx( + window, + title, + message, + buttonFlags, + okButton, + cancelButton, + null, + null, + {} + ); + if (rv != 1) { + let aus = Cc["@mozilla.org/updates/update-service;1"].getService( + Ci.nsIApplicationUpdateService + ); + aus.stopDownload(); + um.cleanupReadyUpdate(); + um.cleanupDownloadingUpdate(); + } + }, + + showUpdates() { + gSubDialog.open("chrome://mozapps/content/update/history.xhtml"); + }, + + _loadAppHandlerData() { + this._loadInternalHandlers(); + this._loadApplicationHandlers(); + }, + + _loadInternalHandlers() { + const internalHandlers = [new PDFHandlerInfoWrapper()]; + for (const internalHandler of internalHandlers) { + if (internalHandler.enabled) { + this._handledTypes.set(internalHandler.type, internalHandler); + } + } + }, + + /** + * Load the set of handlers defined by the application datastore. + */ + _loadApplicationHandlers() { + for (let wrappedHandlerInfo of gHandlerService.enumerate()) { + let type = wrappedHandlerInfo.type; + + let handlerInfoWrapper; + if (this._handledTypes.has(type)) { + handlerInfoWrapper = this._handledTypes.get(type); + } else { + handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo); + this._handledTypes.set(type, handlerInfoWrapper); + } + } + }, + + // ----------------- + // View Construction + + _rebuildVisibleTypes() { + // Reset the list of visible types and the visible type description. + this._visibleTypes.length = 0; + this._visibleDescriptions.clear(); + + for (let handlerInfo of this._handledTypes.values()) { + // We couldn't find any reason to exclude the type, so include it. + this._visibleTypes.push(handlerInfo); + + let otherHandlerInfo = this._visibleDescriptions.get( + handlerInfo.description + ); + 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; + this._visibleDescriptions.set(handlerInfo.description, 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; + } + } + }, + + _rebuildView() { + // Clear the list of entries. + let tbody = this._handlerTbody; + while (tbody.hasChildNodes()) { + // Rows kept alive by the _handlerRows map. + tbody.removeChild(tbody.lastChild); + } + + let sort = this._handlerSort; + for (let header of this._handlerSortHeaders) { + let icon = header.querySelector("img"); + if (sort.type === header.getAttribute("sort-type")) { + icon.setAttribute( + "src", + "chrome://messenger/skin/icons/new/nav-down-sm.svg" + ); + if (sort.descending) { + /* Rotates the src image to point up. */ + icon.setAttribute("descending", ""); + header.setAttribute("aria-sort", "descending"); + } else { + icon.removeAttribute("descending"); + header.setAttribute("aria-sort", "ascending"); + } + } else { + icon.removeAttribute("src"); + header.setAttribute("aria-sort", "none"); + } + } + + let visibleTypes = this._visibleTypes; + + // If the user is filtering the list, then only show matching types. + if (this._filter.value) { + visibleTypes = visibleTypes.filter(this._matchesFilter, this); + } + + for (let handlerInfo of visibleTypes) { + let row = this._handlerRows.get(handlerInfo); + if (row) { + tbody.appendChild(row.node); + } else { + row = new HandlerRow(handlerInfo, this.onDelete.bind(this)); + row.constructNodeAndAppend(tbody, this._handlerMenuId); + this._handlerMenuId++; + this._handlerRows.set(handlerInfo, row); + } + } + }, + + _matchesFilter(aType) { + var filterValue = this._filter.value.toLowerCase(); + return ( + aType.typeDescription.toLowerCase().includes(filterValue) || + aType.actionDescription.toLowerCase().includes(filterValue) + ); + }, + + /** + * Get the details for the type represented by the given handler info + * object. + * + * @param aHandlerInfo {nsIHandlerInfo} the type to get the extensions for. + * @returns {string} the extensions for the type + */ + _typeDetails(aHandlerInfo) { + let exts = []; + if (aHandlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) { + for (let extName of aHandlerInfo.wrappedHandlerInfo.getFileExtensions()) { + let ext = "." + extName; + if (!exts.includes(ext)) { + exts.push(ext); + } + } + } + exts.sort(); + exts = exts.join(", "); + if (this._visibleDescriptions.has(aHandlerInfo.description)) { + if (exts) { + return this._prefsBundle.getFormattedString( + "typeDetailsWithTypeAndExt", + [aHandlerInfo.type, exts] + ); + } + return this._prefsBundle.getFormattedString("typeDetailsWithTypeOrExt", [ + aHandlerInfo.type, + ]); + } + if (exts) { + return this._prefsBundle.getFormattedString("typeDetailsWithTypeOrExt", [ + exts, + ]); + } + return exts; + }, + + /** + * 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.nsIWebContentHandlerInfo) { + return aHandlerApp.uri; + } + + return false; + }, + + _isValidHandlerExecutable(aExecutable) { + let isExecutable = + 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 + if (AppConstants.platform == "win") { + return ( + isExecutable && + aExecutable.leafName != AppConstants.MOZ_APP_NAME + ".exe" + ); + } + + if (AppConstants.platform == "macosx") { + return ( + isExecutable && aExecutable.leafName != AppConstants.MOZ_MACBUNDLE_NAME + ); + } + + return ( + isExecutable && aExecutable.leafName != AppConstants.MOZ_APP_NAME + "-bin" + ); + }, + + // ------------------- + // Sorting & Filtering + + /** + * Sort the list when the user clicks on a column header. If sortType is + * different than the last sort, the sort direction is toggled. Otherwise, the + * sort is changed to the new sortType with ascending direction. + * + * @param {string} sortType - The sort type associated with the column header. + */ + sort(sortType) { + let sort = this._handlerSort; + if (sort.type === sortType) { + sort.descending = !sort.descending; + } else { + sort.type = sortType; + sort.descending = false; + } + this._sortVisibleTypes(); + this._rebuildView(); + }, + + /** + * Sort the list of visible types by the current sort column/direction. + */ + _sortVisibleTypes() { + function sortByType(a, b) { + return a.typeDescription + .toLowerCase() + .localeCompare(b.typeDescription.toLowerCase()); + } + + function sortByAction(a, b) { + return a.actionDescription + .toLowerCase() + .localeCompare(b.actionDescription.toLowerCase()); + } + + let sort = this._handlerSort; + if (sort.type === "action") { + this._visibleTypes.sort(sortByAction); + } else { + this._visibleTypes.sort(sortByType); + } + if (sort.descending) { + this._visibleTypes.reverse(); + } + }, + + focusFilterBox() { + this._filter.focus(); + this._filter.select(); + }, + + onDelete(handlerRow) { + let handlerInfo = handlerRow.handlerInfoWrapper; + let index = this._visibleTypes.indexOf(handlerInfo); + if (index != -1) { + this._visibleTypes.splice(index, 1); + } + + let tbody = this._handlerTbody; + if (handlerRow.node.parentNode === tbody) { + tbody.removeChild(handlerRow.node); + } + + this._handledTypes.delete(handlerInfo.type); + this._handlerRows.delete(handlerInfo); + + handlerInfo.remove(); + }, + + _getIconURLForHandlerApp(aHandlerApp) { + if (aHandlerApp instanceof Ci.nsILocalHandlerApp) { + return this._getIconURLForFile(aHandlerApp.executable); + } + + if (aHandlerApp instanceof Ci.nsIWebHandlerApp) { + return this._getIconURLForWebApp(aHandlerApp.uriTemplate); + } + + if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo) { + return this._getIconURLForWebApp(aHandlerApp.uri); + } + + // We know nothing about other kinds of handler apps. + return ""; + }, + + _getIconURLForFile(aFile) { + let urlSpec = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler) + .getURLSpecFromActualFile(aFile); + + return "moz-icon://" + urlSpec + "?size=16"; + }, + + _getIconURLForWebApp(aWebAppURITemplate) { + var uri = Services.io.newURI(aWebAppURITemplate); + + // Unfortunately we can't use the favicon service to get the favicon, + // because the service looks in the annotations table for a record with + // the exact URL we give it, and users won't have such records for URLs + // they don't visit, and users won't visit the web app's URL template, + // they'll only visit URLs derived from that template (i.e. with %s + // in the template replaced by the URL of the content being handled). + + if (/^https?/.test(uri.scheme)) { + return uri.prePath + "/favicon.ico"; + } + + return /^https?/.test(uri.scheme) ? uri.resolve("/favicon.ico") : ""; + }, + + destroy() { + window.removeEventListener("unload", this); + + Services.obs.removeObserver(this, AUTO_UPDATE_CHANGED_TOPIC); + Services.prefs.removeObserver("mailnews.tags.", this); + }, + + // nsISupports + + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + // nsIObserver + + async observe(subject, topic, data) { + if (topic == AUTO_UPDATE_CHANGED_TOPIC) { + if (data != "true" && data != "false") { + throw new Error(`Invalid value for app.update.auto ${data}`); + } + document.getElementById("updateRadioGroup").value = data; + } else if (topic == "nsPref:changed" && data.startsWith("mailnews.tags.")) { + let selIndex = this.mTagListBox.selectedIndex; + this.mTagListBox.replaceChildren(); + this.buildTagList(); + let numItemsInListBox = this.mTagListBox.getRowCount(); + this.mTagListBox.selectedIndex = + selIndex < numItemsInListBox ? selIndex : numItemsInListBox - 1; + if (data.endsWith(".color") && Services.prefs.prefHasUserValue(data)) { + let key = data.replace(/^mailnews\.tags\./, "").replace(/\.color$/, ""); + let color = Services.prefs.getCharPref(`mailnews.tags.${key}.color`); + // Add to style sheet. We simply add the new color, the rule is added + // at the end and will overrule the previous rule. + TagUtils.addTagToAllDocumentSheets(key, color); + } + } + }, + + // EventListener + + handleEvent(aEvent) { + if (aEvent.type == "unload") { + this.destroy(); + if (AppConstants.MOZ_UPDATER) { + onUnload(); + } + } + }, +}; + +function getDisplayNameForFile(aFile) { + if (AppConstants.platform == "win") { + if (aFile instanceof Ci.nsILocalFileWin) { + try { + return aFile.getVersionInfoField("FileDescription"); + } catch (ex) { + // fall through to the file name + } + } + } else if (AppConstants.platform == "macosx") { + if (aFile instanceof Ci.nsILocalFileMac) { + try { + return aFile.bundleDisplayName; + } catch (ex) { + // fall through to the file name + } + } + } + + return aFile.leafName; +} + +function getLocalHandlerApp(aFile) { + var localHandlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + localHandlerApp.name = getDisplayNameForFile(aFile); + localHandlerApp.executable = aFile; + + return localHandlerApp; +} + +// eslint-disable-next-line no-undef +let gHandlerRowFragment = MozXULElement.parseXULToFragment(` + <html:tr> + <html:td class="typeCell"> + <html:div class="typeLabel"> + <html:img class="typeIcon" alt=""/> + <label class="typeDescription" crop="end"/> + </html:div> + </html:td> + <html:td class="actionCell"> + <menulist class="actionsMenu" crop="end" selectedIndex="1"> + <menupopup/> + </menulist> + </html:td> + </html:tr> +`); + +/** + * This is associated to rows in the handlers table. + */ +class HandlerRow { + constructor(handlerInfoWrapper, onDeleteCallback) { + this.handlerInfoWrapper = handlerInfoWrapper; + this.previousSelectedItem = null; + this.deleteCallback = onDeleteCallback; + } + + constructNodeAndAppend(tbody, id) { + tbody.appendChild(document.importNode(gHandlerRowFragment, true)); + this.node = tbody.lastChild; + + this.menu = this.node.querySelector(".actionsMenu"); + id = `action-menu-${id}`; + this.menu.setAttribute("id", id); + this.menu.addEventListener("command", event => + this.onSelectAction(event.originalTarget) + ); + + let typeDescription = this.node.querySelector(".typeDescription"); + typeDescription.setAttribute( + "value", + this.handlerInfoWrapper.typeDescription + ); + // NOTE: Control only works for a XUL <label>. Using a HTML <label> and the + // corresponding "for" attribute would not currently work with the XUL + // <menulist> because a XUL <menulist> is technically not a labelable + // element, as required for the html:label "for" attribute. + typeDescription.setAttribute("control", id); + // Spoof the HTML label "for" attribute focus behaviour on the whole cell. + this.node + .querySelector(".typeCell") + .addEventListener("click", () => this.menu.focus()); + + this.node + .querySelector(".typeIcon") + .setAttribute("src", this.handlerInfoWrapper.smallIcon); + + this.rebuildActionsMenu(); + } + + rebuildActionsMenu() { + let menu = this.menu; + let menuPopup = menu.menupopup; + let handlerInfo = this.handlerInfoWrapper; + + // Clear out existing items. + while (menuPopup.hasChildNodes()) { + menuPopup.removeChild(menuPopup.lastChild); + } + + let internalMenuItem; + // Add the "Preview in Thunderbird" option for optional internal handlers. + if (handlerInfo instanceof InternalHandlerInfoWrapper) { + internalMenuItem = document.createXULElement("menuitem"); + internalMenuItem.setAttribute( + "action", + Ci.nsIHandlerInfo.handleInternally + ); + let label = gGeneralPane._prefsBundle.getFormattedString("previewInApp", [ + gGeneralPane._brandShortName, + ]); + internalMenuItem.setAttribute("label", label); + internalMenuItem.setAttribute("tooltiptext", label); + internalMenuItem.setAttribute( + "image", + "chrome://messenger/skin/preferences/alwaysAsk.png" + ); + menuPopup.appendChild(internalMenuItem); + } + + let askMenuItem = document.createXULElement("menuitem"); + askMenuItem.setAttribute("alwaysAsk", "true"); + { + let label = gGeneralPane._prefsBundle.getString("alwaysAsk"); + askMenuItem.setAttribute("label", label); + askMenuItem.setAttribute("tooltiptext", label); + askMenuItem.setAttribute( + "image", + "chrome://messenger/skin/preferences/alwaysAsk.png" + ); + 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. + let saveMenuItem; + if (handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) { + saveMenuItem = document.createXULElement("menuitem"); + saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk); + let label = gGeneralPane._prefsBundle.getString("saveFile"); + saveMenuItem.setAttribute("label", label); + saveMenuItem.setAttribute("tooltiptext", label); + saveMenuItem.setAttribute( + "image", + "chrome://messenger/skin/preferences/saveFile.png" + ); + menuPopup.appendChild(saveMenuItem); + } + + // Add a separator to distinguish these items from the helper app items + // that follow them. + let menuItem = document.createXULElement("menuseparator"); + menuPopup.appendChild(menuItem); + + // Create a menu item for the OS default application, if any. + let defaultMenuItem; + if (handlerInfo.hasDefaultHandler) { + defaultMenuItem = document.createXULElement("menuitem"); + defaultMenuItem.setAttribute( + "action", + Ci.nsIHandlerInfo.useSystemDefault + ); + let label = gGeneralPane._prefsBundle.getFormattedString("useDefault", [ + handlerInfo.defaultDescription, + ]); + defaultMenuItem.setAttribute("label", label); + defaultMenuItem.setAttribute( + "tooltiptext", + handlerInfo.defaultDescription + ); + defaultMenuItem.setAttribute( + "image", + handlerInfo.iconURLForSystemDefault + ); + + menuPopup.appendChild(defaultMenuItem); + } + + // Create menu items for possible handlers. + let preferredApp = handlerInfo.preferredApplicationHandler; + let possibleAppMenuItems = []; + for (let possibleApp of handlerInfo.possibleApplicationHandlers.enumerate()) { + if (!gGeneralPane.isValidHandlerApp(possibleApp)) { + continue; + } + + let menuItem = document.createXULElement("menuitem"); + menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp); + let label; + if (possibleApp instanceof Ci.nsILocalHandlerApp) { + label = getDisplayNameForFile(possibleApp.executable); + } else { + label = possibleApp.name; + } + label = gGeneralPane._prefsBundle.getFormattedString("useApp", [label]); + menuItem.setAttribute("label", label); + menuItem.setAttribute("tooltiptext", label); + menuItem.setAttribute( + "image", + gGeneralPane._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); + } + + // Create a menu item for selecting a local application. + let createItem = 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"); + if (handlerInfo.type == executableType) { + createItem = false; + } + } + + if (createItem) { + let menuItem = document.createXULElement("menuitem"); + menuItem.addEventListener("command", this.chooseApp.bind(this)); + let label = gGeneralPane._prefsBundle.getString("useOtherApp"); + menuItem.setAttribute("label", label); + menuItem.setAttribute("tooltiptext", label); + 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.addEventListener("command", this.manageApp.bind(this)); + menuItem.setAttribute( + "label", + gGeneralPane._prefsBundle.getString("manageApp") + ); + menuPopup.appendChild(menuItem); + } + + menuItem = document.createXULElement("menuseparator"); + menuPopup.appendChild(menuItem); + menuItem = document.createXULElement("menuitem"); + menuItem.addEventListener("command", this.confirmDelete.bind(this)); + menuItem.setAttribute( + "label", + gGeneralPane._prefsBundle.getString("delete") + ); + 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 { + console.error("No menu item defined to set!"); + } + break; + case Ci.nsIHandlerInfo.useSystemDefault: + menu.selectedItem = defaultMenuItem; + break; + case Ci.nsIHandlerInfo.useHelperApp: + if (preferredApp) { + menu.selectedItem = possibleAppMenuItems.filter(v => + v.handlerApp.equals(preferredApp) + )[0]; + } + break; + case Ci.nsIHandlerInfo.saveToDisk: + menu.selectedItem = saveMenuItem; + break; + } + } + // menu.selectedItem may be null if the preferredAction is + // useSystemDefault, but handlerInfo.hasDefaultHandler returns false. + // For now, we'll just use the askMenuItem to avoid ugly exceptions. + this.previousSelectedItem = this.menu.selectedItem || askMenuItem; + } + + 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.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(); + }; + + gSubDialog.open( + "chrome://messenger/content/preferences/applicationManager.xhtml", + { features: "resizable=no", closingCallback: onComplete }, + handlerInfo + ); + } + + 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 onSelectionDone = function () { + // 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 (handlerApp) { + let menuItems = this.menu.menupopup.children; + for (let i = 0; i < menuItems.length; i++) { + let menuItem = menuItems[i]; + if (menuItem.handlerApp && menuItem.handlerApp.equals(handlerApp)) { + this.menu.selectedIndex = i; + this.onSelectAction(menuItem); + break; + } + } + } + }.bind(this); + + if (AppConstants.platform == "win") { + let params = {}; + let handlerInfo = this.handlerInfoWrapper; + + params.mimeInfo = handlerInfo.wrappedHandlerInfo; + + params.title = gGeneralPane._prefsBundle.getString("fpTitleChooseApp"); + params.description = handlerInfo.description; + params.filename = null; + params.handlerApp = null; + + let onAppSelected = () => { + if (gGeneralPane.isValidHandlerApp(params.handlerApp)) { + handlerApp = params.handlerApp; + + // Add the app to the type's list of possible handlers. + handlerInfo.addPossibleApplicationHandler(handlerApp); + } + onSelectionDone(); + }; + + gSubDialog.open( + "chrome://global/content/appPicker.xhtml", + { features: "resizable=no", closingCallback: onAppSelected }, + params + ); + } else { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + let winTitle = gGeneralPane._prefsBundle.getString("fpTitleChooseApp"); + fp.init(window, winTitle, Ci.nsIFilePicker.modeOpen); + fp.appendFilters(Ci.nsIFilePicker.filterApps); + + // 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.open(rv => { + if ( + rv == Ci.nsIFilePicker.returnOK && + fp.file && + gGeneralPane._isValidHandlerExecutable(fp.file) + ) { + handlerApp = Cc[ + "@mozilla.org/uriloader/local-handler-app;1" + ].createInstance(Ci.nsILocalHandlerApp); + handlerApp.name = getDisplayNameForFile(fp.file); + handlerApp.executable = fp.file; + + // Add the app to the type's list of possible handlers. + let handlerInfo = this.handlerInfoWrapper; + handlerInfo.addPossibleApplicationHandler(handlerApp); + } + onSelectionDone(); + }); + } + } + + confirmDelete(aEvent) { + aEvent.stopPropagation(); + if ( + Services.prompt.confirm( + null, + gGeneralPane._prefsBundle.getString("confirmDeleteTitle"), + gGeneralPane._prefsBundle.getString("confirmDeleteText") + ) + ) { + // Deletes self. + this.deleteCallback(this); + } else { + // They hit cancel, so return them to the previously selected item. + this.menu.selectedItem = this.previousSelectedItem; + } + } + + onSelectAction(aActionItem) { + this.previousSelectedItem = aActionItem; + this._storeAction(aActionItem); + } + + _storeAction(aActionItem) { + var handlerInfo = this.handlerInfoWrapper; + + if (aActionItem.hasAttribute("alwaysAsk")) { + handlerInfo.alwaysAskBeforeHandling = true; + } else if (aActionItem.hasAttribute("action")) { + let action = parseInt(aActionItem.getAttribute("action")); + + // Set the preferred application handler. + // We leave the existing preferred app in the list when we set + // the preferred action to something other than useHelperApp so that + // legacy datastores that don't have the preferred app in the list + // of possible apps still include the preferred app in the list of apps + // the user can choose to handle the type. + if (action == Ci.nsIHandlerInfo.useHelperApp) { + handlerInfo.preferredApplicationHandler = aActionItem.handlerApp; + } + + // Set the "always ask" flag. + handlerInfo.alwaysAskBeforeHandling = false; + + // Set the preferred action. + handlerInfo.preferredAction = action; + } + + handlerInfo.store(); + } +} + +/** + * This object wraps nsIHandlerInfo with some additional functionality + * the Applications prefpane needs to display and allow modification of + * the list of handled types. + * + * We create an instance of this wrapper for each entry we might display + * in the prefpane, and we compose the instances from various sources, + * including the handler service. + * + * We don't implement all the original nsIHandlerInfo functionality, + * just the stuff that the prefpane needs. + */ +class HandlerInfoWrapper { + constructor(type, handlerInfo) { + this.type = type; + this.wrappedHandlerInfo = handlerInfo; + this.disambiguateDescription = false; + } + + get description() { + if (this.wrappedHandlerInfo.description) { + return this.wrappedHandlerInfo.description; + } + + if (this.primaryExtension) { + var extension = this.primaryExtension.toUpperCase(); + return document + .getElementById("bundlePreferences") + .getFormattedString("fileEnding", [extension]); + } + return 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) { + return gGeneralPane._prefsBundle.getFormattedString( + "typeDetailsWithTypeAndExt", + [this.description, this.type] + ); + } + + return this.description; + } + + /** + * Describe, in a human-readable fashion, the preferred action to take on + * the type represented by the given handler info object. + */ + get actionDescription() { + // alwaysAskBeforeHandling overrides the preferred action, so if that flag + // is set, then describe that behavior instead. For most types, this is + // the "alwaysAsk" string, but for the feed type we show something special. + if (this.alwaysAskBeforeHandling) { + return gGeneralPane._prefsBundle.getString("alwaysAsk"); + } + + switch (this.preferredAction) { + case Ci.nsIHandlerInfo.saveToDisk: + return gGeneralPane._prefsBundle.getString("saveFile"); + + case Ci.nsIHandlerInfo.useHelperApp: + var preferredApp = this.preferredApplicationHandler; + var name; + if (preferredApp instanceof Ci.nsILocalHandlerApp) { + name = getDisplayNameForFile(preferredApp.executable); + } else { + name = preferredApp.name; + } + return gGeneralPane._prefsBundle.getFormattedString("useApp", [name]); + + case Ci.nsIHandlerInfo.handleInternally: + if (this instanceof InternalHandlerInfoWrapper) { + return gGeneralPane._prefsBundle.getFormattedString("previewInApp", [ + gGeneralPane._brandShortName, + ]); + } + + // For other types, handleInternally looks like either useHelperApp + // or useSystemDefault depending on whether or not there's a preferred + // handler app. + if (gGeneralPane.isValidHandlerApp(this.preferredApplicationHandler)) { + return this.preferredApplicationHandler.name; + } + + return this.defaultDescription; + + // XXX Why don't we say the app will handle the type internally? + // Is it because the app can't actually do that? But if that's true, + // then why would a preferredAction ever get set to this value + // in the first place? + + case Ci.nsIHandlerInfo.useSystemDefault: + return gGeneralPane._prefsBundle.getFormattedString("useDefault", [ + this.defaultDescription, + ]); + + default: + throw new Error(`Unexpected preferredAction: ${this.preferredAction}`); + } + } + + 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 "ask"; + } + } + + return ""; + } + + get actionIcon() { + switch (this.preferredAction) { + case Ci.nsIHandlerInfo.useSystemDefault: + return this.iconURLForSystemDefault; + + case Ci.nsIHandlerInfo.useHelperApp: + let preferredApp = this.preferredApplicationHandler; + if (gGeneralPane.isValidHandlerApp(preferredApp)) { + return gGeneralPane._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 possibleApp of this.possibleApplicationHandlers.enumerate()) { + if (possibleApp.equals(aNewHandler)) { + return; + } + } + this.possibleApplicationHandlers.appendElement(aNewHandler); + } + + removePossibleApplicationHandler(aHandler) { + var defaultApp = this.preferredApplicationHandler; + if (defaultApp && aHandler.equals(defaultApp)) { + // If the app we remove was the default app, we must make sure + // it won't be used anymore + this.alwaysAskBeforeHandling = true; + this.preferredApplicationHandler = null; + } + + var handlers = this.possibleApplicationHandlers; + for (var i = 0; i < handlers.length; ++i) { + var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp); + if (handler.equals(aHandler)) { + handlers.removeElementAt(i); + break; + } + } + } + + get hasDefaultHandler() { + return this.wrappedHandlerInfo.hasDefaultHandler; + } + + get defaultDescription() { + return this.wrappedHandlerInfo.defaultDescription; + } + + // What to do with content of this type. + get preferredAction() { + // If the action is to use a helper app, but we don't have a preferred + // handler app, then switch to using the system default, if any; otherwise + // fall back to saving to disk, which is the default action in nsMIMEInfo. + // Note: "save to disk" is an invalid value for protocol info objects, + // but the alwaysAskBeforeHandling getter will detect that situation + // and always return true in that case to override this invalid value. + if ( + this.wrappedHandlerInfo.preferredAction == + Ci.nsIHandlerInfo.useHelperApp && + !gGeneralPane.isValidHandlerApp(this.preferredApplicationHandler) + ) { + if (this.wrappedHandlerInfo.hasDefaultHandler) { + return Ci.nsIHandlerInfo.useSystemDefault; + } + return Ci.nsIHandlerInfo.saveToDisk; + } + + return this.wrappedHandlerInfo.preferredAction; + } + + set preferredAction(aNewValue) { + this.wrappedHandlerInfo.preferredAction = aNewValue; + } + + get alwaysAskBeforeHandling() { + // If this is a protocol type and the preferred action is "save to disk", + // which is invalid for such types, then return true here to override that + // action. This could happen when the preferred action is to use a helper + // app, but the preferredApplicationHandler is invalid, and there isn't + // a default handler, so the preferredAction getter returns save to disk + // instead. + if ( + !(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) && + this.preferredAction == Ci.nsIHandlerInfo.saveToDisk + ) { + return true; + } + + return this.wrappedHandlerInfo.alwaysAskBeforeHandling; + } + + set alwaysAskBeforeHandling(aNewValue) { + this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue; + } + + // The primary file extension associated with this type, if any. + get primaryExtension() { + try { + if ( + this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo && + this.wrappedHandlerInfo.primaryExtension + ) { + return this.wrappedHandlerInfo.primaryExtension; + } + } catch (ex) {} + + return null; + } + + // ------- + // Storage + + store() { + gHandlerService.store(this.wrappedHandlerInfo); + } + + remove() { + gHandlerService.remove(this.wrappedHandlerInfo); + } + + // ----- + // Icons + + get smallIcon() { + return this._getIcon(16); + } + + get largeIcon() { + return this._getIcon(32); + } + + _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) { + super(mimeType, gMIMEService.getFromTypeAndExtension(mimeType, null)); + } + + // 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); + } + + get description() { + return gGeneralPane._prefsBundle.getString(this._appPrefLabel); + } +} + +class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper { + constructor() { + super(TYPE_PDF); + } + + get _appPrefLabel() { + return "applications-type-pdf"; + } + + get enabled() { + return !Services.prefs.getBoolPref(PREF_PDFJS_DISABLED); + } +} + +function addTagCallback(aName, aColor) { + MailServices.tags.addTag(aName, aColor, ""); + + // Add to style sheet. + let key = MailServices.tags.getKeyForTag(aName); + let tagListBox = document.getElementById("tagList"); + let item = tagListBox.querySelector(`richlistitem[value=${key}]`); + tagListBox.ensureElementIsVisible(item); + tagListBox.selectItem(item); + tagListBox.focus(); + return true; +} + +Preferences.get("mailnews.start_page.enabled").on( + "change", + gGeneralPane.updateStartPage +); +Preferences.get("font.language.group").on("change", gGeneralPane._rebuildFonts); +Preferences.get("mailnews.mark_message_read.auto").on( + "change", + gGeneralPane.updateMarkAsReadOptions +); +Preferences.get("mailnews.mark_message_read.delay").on( + "change", + gGeneralPane.updateMarkAsReadTextbox +); +Preferences.get("mail.prompt_purge_threshhold").on( + "change", + gGeneralPane.updateCompactOptions +); +Preferences.get("layers.acceleration.disabled").on( + "change", + gGeneralPane.updateHardwareAcceleration +); +if (AppConstants.platform != "macosx") { + Preferences.get("mail.biff.show_alert").on( + "change", + gGeneralPane.updateShowAlert + ); +} diff --git a/comm/mail/components/preferences/jar.mn b/comm/mail/components/preferences/jar.mn new file mode 100644 index 0000000000..fc9b56184e --- /dev/null +++ b/comm/mail/components/preferences/jar.mn @@ -0,0 +1,55 @@ +# 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/. + +messenger.jar: +* content/messenger/preferences/preferences.xhtml + content/messenger/preferences/preferences.js + content/messenger/preferences/preferencesTab.js + content/messenger/preferences/general.js +#if defined(XP_MACOSX) || defined(XP_WIN) + content/messenger/preferences/dockoptions.js +* content/messenger/preferences/dockoptions.xhtml +#endif + content/messenger/preferences/chat.js +#ifdef NIGHTLY_BUILD + content/messenger/preferences/sync.js +#endif + content/messenger/preferences/messagestyle.js + content/messenger/preferences/messengerLanguages.js + content/messenger/preferences/messengerLanguages.xhtml + content/messenger/preferences/colors.js +* content/messenger/preferences/colors.xhtml + content/messenger/preferences/compose.js + content/messenger/preferences/extensionControlled.js + content/messenger/preferences/privacy.js + content/messenger/preferences/receipts.js + content/messenger/preferences/receipts.xhtml + content/messenger/preferences/connection.js + content/messenger/preferences/connection.xhtml + content/messenger/preferences/downloads.js + content/messenger/preferences/attachmentReminder.js + content/messenger/preferences/attachmentReminder.xhtml + content/messenger/preferences/applicationManager.xhtml + content/messenger/preferences/applicationManager.js + content/messenger/preferences/actionsshared.js + content/messenger/preferences/findInPage.js + content/messenger/preferences/fonts.js + content/messenger/preferences/fonts.xhtml +#ifndef XP_MACOSX + content/messenger/preferences/notifications.js + content/messenger/preferences/notifications.xhtml +#endif + content/messenger/preferences/offline.js + content/messenger/preferences/offline.xhtml + content/messenger/preferences/cookies.js +* content/messenger/preferences/cookies.xhtml + content/messenger/preferences/passwordManager.js + content/messenger/preferences/passwordManager.xhtml + content/messenger/preferences/permissions.js + content/messenger/preferences/permissions.xhtml +#ifdef NIGHTLY_BUILD + content/messenger/preferences/syncDialog.js + content/messenger/preferences/syncDialog.xhtml +#endif +* content/messenger/preferences/tagDialog.xhtml diff --git a/comm/mail/components/preferences/messagestyle.js b/comm/mail/components/preferences/messagestyle.js new file mode 100644 index 0000000000..7d10553296 --- /dev/null +++ b/comm/mail/components/preferences/messagestyle.js @@ -0,0 +1,259 @@ +/* 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 preferences.js */ + +var { GenericConvIMPrototype, GenericMessagePrototype } = + ChromeUtils.importESModule("resource:///modules/jsProtoHelper.sys.mjs"); +var { getThemeByName, getThemeVariants } = ChromeUtils.importESModule( + "resource:///modules/imThemes.sys.mjs" +); + +var { IMServices } = ChromeUtils.importESModule( + "resource:///modules/IMServices.sys.mjs" +); + +function Conversation(aName) { + this._name = aName; + this._observers = []; + let now = new Date(); + this._date = + new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 42, 22) * + 1000; +} +Conversation.prototype = { + __proto__: GenericConvIMPrototype, + account: { + protocol: { name: "Fake Protocol" }, + alias: "", + name: "Fake Account", + get statusInfo() { + return IMServices.core.globalUserStatus; + }, + }, +}; + +function Message(aWho, aMessage, aObject, aConversation) { + this._init(aWho, aMessage, aObject, aConversation); +} +Message.prototype = { + __proto__: GenericMessagePrototype, + get displayMessage() { + return this.originalMessage; + }, +}; + +// Message style tooltips use this. +function getBrowser() { + return document.getElementById("previewbrowser"); +} + +var previewObserver = { + _loaded: false, + load() { + let makeDate = function (aDateString) { + let array = aDateString.split(":"); + let now = new Date(); + return ( + new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + array[0], + array[1], + array[2] + ) / 1000 + ); + }; + let bundle = document.getElementById("themesBundle"); + let msg = {}; + [ + "nick1", + "buddy1", + "nick2", + "buddy2", + "message1", + "message2", + "message3", + ].forEach(function (aText) { + msg[aText] = bundle.getString(aText); + }); + let conv = new Conversation(msg.nick2); + conv.messages = [ + new Message( + msg.buddy1, + msg.message1, + { + outgoing: true, + _alias: msg.nick1, + time: makeDate("10:42:22"), + }, + conv + ), + new Message( + msg.buddy1, + msg.message2, + { + outgoing: true, + _alias: msg.nick1, + time: makeDate("10:42:25"), + }, + conv + ), + new Message( + msg.buddy2, + msg.message3, + { + incoming: true, + _alias: msg.nick2, + time: makeDate("10:43:01"), + }, + conv + ), + ]; + previewObserver.conv = conv; + + let themeName = document.getElementById("messagestyle-themename"); + previewObserver.browser = document.getElementById("previewbrowser"); + + // If the preferences tab is opened straight to the message styles, + // loading the preview fails. Pushing this to back of the event queue + // prevents that failure. + setTimeout(() => { + previewObserver.displayTheme(themeName.value); + this._loaded = true; + }); + }, + + currentThemeChanged() { + if (!this._loaded) { + return; + } + + let currentTheme = document.getElementById("messagestyle-themename").value; + if (!currentTheme) { + return; + } + + this.displayTheme(currentTheme); + }, + + _ignoreVariantChange: false, + currentVariantChanged() { + if (!this._loaded || this._ignoreVariantChange) { + return; + } + + let variant = document.getElementById("themevariant").value; + if (!variant) { + return; + } + + this.theme.variant = variant; + this.reloadPreview(); + }, + + displayTheme(aTheme) { + try { + this.theme = getThemeByName(aTheme); + } catch (e) { + let previewBoxBrowser = document + .getElementById("previewBox") + .querySelector("browser"); + if (previewBoxBrowser) { + previewBoxBrowser.hidden = true; + } + document.getElementById("noPreviewScreen").hidden = false; + return; + } + + let menulist = document.getElementById("themevariant"); + if (menulist.menupopup) { + menulist.menupopup.remove(); + } + let popup = menulist.appendChild(document.createXULElement("menupopup")); + let variants = getThemeVariants(this.theme); + + let defaultVariant = ""; + if ( + "DefaultVariant" in this.theme.metadata && + variants.includes(this.theme.metadata.DefaultVariant) + ) { + defaultVariant = this.theme.metadata.DefaultVariant.replace(/_/g, " "); + } + + let defaultText = defaultVariant; + if (!defaultText && "DisplayNameForNoVariant" in this.theme.metadata) { + defaultText = this.theme.metadata.DisplayNameForNoVariant; + } + // if the name in the metadata is 'Default', use the localized version + if (!defaultText || defaultText.toLowerCase() == "default") { + defaultText = document + .getElementById("themesBundle") + .getString("default"); + } + + let menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("label", defaultText); + menuitem.setAttribute("value", "default"); + popup.appendChild(menuitem); + popup.appendChild(document.createXULElement("menuseparator")); + + variants.sort().forEach(function (aVariantName) { + let displayName = aVariantName.replace(/_/g, " "); + if (displayName != defaultVariant) { + let menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("label", displayName); + menuitem.setAttribute("value", aVariantName); + popup.appendChild(menuitem); + } + }); + this._ignoreVariantChange = true; + if (!this._loaded) { + menulist.value = this.theme.variant = menulist.value; + } else { + menulist.value = this.theme.variant; // (reset to "default") + Preferences.userChangedValue(menulist); + } + this._ignoreVariantChange = false; + + // disable the variant menulist if there's no variant, or only one + // which is the default + menulist.disabled = + variants.length == 0 || (variants.length == 1 && defaultVariant); + + this.reloadPreview(); + document.getElementById("noPreviewScreen").hidden = true; + }, + + reloadPreview() { + this.browser.init(this.conv); + this.browser._theme = this.theme; + Services.obs.addObserver(this, "conversation-loaded"); + }, + + observe(aSubject, aTopic, aData) { + if (aTopic != "conversation-loaded" || aSubject != this.browser) { + return; + } + + // We want to avoid the convbrowser trying to scroll to the last + // added message, as that causes the entire pref pane to jump up + // (bug 1179943). Therefore, we override the method convbrowser + // uses to determine if it should scroll, as well as its + // mirror in the contentWindow (that messagestyle JS can call). + this.browser.convScrollEnabled = () => false; + this.browser.contentWindow.convScrollEnabled = () => false; + + // Display all queued messages. Use a timeout so that message text + // modifiers can be added with observers for this notification. + setTimeout(function () { + for (let message of previewObserver.conv.messages) { + aSubject.appendMessage(message, false); + } + }, 0); + + Services.obs.removeObserver(this, "conversation-loaded"); + }, +}; diff --git a/comm/mail/components/preferences/messengerLanguages.js b/comm/mail/components/preferences/messengerLanguages.js new file mode 100644 index 0000000000..e66f5f7fd0 --- /dev/null +++ b/comm/mail/components/preferences/messengerLanguages.js @@ -0,0 +1,632 @@ +/* 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/. */ + +// This is exported by preferences.js but we can't import that in a subdialog. +let { getAvailableLocales } = window.top; + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs", +}); + +/* This dialog provides an interface for managing what language the messenger is + * displayed in. + * + * There is a list of "requested" locales and a list of "available" locales. The + * requested locales must be installed and enabled. Available locales could be + * installed and enabled, or fetched from the AMO language tools API. + * + * If a langpack is disabled, there is no way to determine what locale it is for and + * it will only be listed as available if that locale is also available on AMO and + * the user has opted to search for more languages. + */ + +class OrderedListBox { + constructor({ richlistbox, upButton, downButton, removeButton, onRemove }) { + this.richlistbox = richlistbox; + this.upButton = upButton; + this.downButton = downButton; + this.removeButton = removeButton; + this.onRemove = onRemove; + + this.items = []; + + this.richlistbox.addEventListener("select", () => this.setButtonState()); + this.upButton.addEventListener("command", () => this.moveUp()); + this.downButton.addEventListener("command", () => this.moveDown()); + this.removeButton.addEventListener("command", () => this.removeItem()); + } + + get selectedItem() { + return this.items[this.richlistbox.selectedIndex]; + } + + setButtonState() { + let { upButton, downButton, removeButton } = this; + let { selectedIndex, itemCount } = this.richlistbox; + upButton.disabled = selectedIndex <= 0; + downButton.disabled = selectedIndex == itemCount - 1; + removeButton.disabled = itemCount <= 1 || !this.selectedItem.canRemove; + } + + moveUp() { + let { selectedIndex } = this.richlistbox; + if (selectedIndex == 0) { + return; + } + let { items } = this; + let selectedItem = items[selectedIndex]; + let prevItem = items[selectedIndex - 1]; + items[selectedIndex - 1] = items[selectedIndex]; + items[selectedIndex] = prevItem; + let prevEl = document.getElementById(prevItem.id); + let selectedEl = document.getElementById(selectedItem.id); + this.richlistbox.insertBefore(selectedEl, prevEl); + this.richlistbox.ensureElementIsVisible(selectedEl); + this.setButtonState(); + } + + moveDown() { + let { selectedIndex } = this.richlistbox; + if (selectedIndex == this.items.length - 1) { + return; + } + let { items } = this; + let selectedItem = items[selectedIndex]; + let nextItem = items[selectedIndex + 1]; + items[selectedIndex + 1] = items[selectedIndex]; + items[selectedIndex] = nextItem; + let nextEl = document.getElementById(nextItem.id); + let selectedEl = document.getElementById(selectedItem.id); + this.richlistbox.insertBefore(nextEl, selectedEl); + this.richlistbox.ensureElementIsVisible(selectedEl); + this.setButtonState(); + } + + removeItem() { + let { selectedIndex } = this.richlistbox; + + if (selectedIndex == -1) { + return; + } + + let [item] = this.items.splice(selectedIndex, 1); + this.richlistbox.selectedItem.remove(); + this.richlistbox.selectedIndex = Math.min( + selectedIndex, + this.richlistbox.itemCount - 1 + ); + this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem); + this.onRemove(item); + } + + setItems(items) { + this.items = items; + this.populate(); + this.setButtonState(); + } + + /** + * Add an item to the top of the ordered list. + * + * @param {object} item The item to insert. + */ + addItem(item) { + this.items.unshift(item); + this.richlistbox.insertBefore( + this.createItem(item), + this.richlistbox.firstElementChild + ); + this.richlistbox.selectedIndex = 0; + this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem); + } + + populate() { + this.richlistbox.textContent = ""; + + let frag = document.createDocumentFragment(); + for (let item of this.items) { + frag.appendChild(this.createItem(item)); + } + this.richlistbox.appendChild(frag); + + this.richlistbox.selectedIndex = 0; + this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem); + } + + createItem({ id, label, value }) { + let listitem = document.createXULElement("richlistitem"); + listitem.id = id; + listitem.setAttribute("value", value); + + let labelEl = document.createXULElement("label"); + labelEl.textContent = label; + listitem.appendChild(labelEl); + + return listitem; + } +} + +/** + * The sorted select list of Locales available for the app. + */ +class SortedItemSelectList { + constructor({ menulist, button, onSelect, onChange, compareFn }) { + /** @type {XULElement} */ + this.menulist = menulist; + + /** @type {XULElement} */ + this.popup = menulist.menupopup; + + /** @type {XULElement} */ + this.button = button; + + /** @type {(a: LocaleDisplayInfo, b: LocaleDisplayInfo) => number} */ + this.compareFn = compareFn; + + /** @type {Array<LocaleDisplayInfo>} */ + this.items = []; + + menulist.addEventListener("command", () => { + button.disabled = !menulist.selectedItem; + if (menulist.selectedItem) { + onChange(this.items[menulist.selectedIndex]); + } + }); + button.addEventListener("command", () => { + if (!menulist.selectedItem) { + return; + } + + let [item] = this.items.splice(menulist.selectedIndex, 1); + menulist.selectedItem.remove(); + menulist.setAttribute("label", menulist.getAttribute("placeholder")); + button.disabled = true; + menulist.disabled = menulist.itemCount == 0; + menulist.selectedIndex = -1; + + onSelect(item); + }); + } + + /** + * @param {Array<LocaleDisplayInfo>} items + */ + setItems(items) { + this.items = items.sort(this.compareFn); + this.populate(); + } + + populate() { + let { button, items, menulist, popup } = this; + popup.textContent = ""; + + let frag = document.createDocumentFragment(); + for (let item of items) { + frag.appendChild(this.createItem(item)); + } + popup.appendChild(frag); + + menulist.setAttribute("label", menulist.getAttribute("placeholder")); + menulist.disabled = menulist.itemCount == 0; + menulist.selectedIndex = -1; + button.disabled = true; + } + + /** + * Add an item to the list sorted by the label. + * + * @param {object} item The item to insert. + */ + addItem(item) { + let { compareFn, items, menulist, popup } = this; + + // Find the index of the item to insert before. + let i = items.findIndex(el => compareFn(el, item) >= 0); + items.splice(i, 0, item); + popup.insertBefore(this.createItem(item), menulist.getItemAtIndex(i)); + menulist.disabled = menulist.itemCount == 0; + } + + createItem({ label, value, className, disabled }) { + let item = document.createXULElement("menuitem"); + item.setAttribute("label", label); + if (value) { + item.value = value; + } + if (className) { + item.classList.add(className); + } + if (disabled) { + item.setAttribute("disabled", "true"); + } + return item; + } + + /** + * Disable the inputs and set a data-l10n-id on the menulist. This can be + * reverted with `enableWithMessageId()`. + */ + disableWithMessageId(messageId) { + this.menulist.setAttribute("data-l10n-id", messageId); + this.menulist.setAttribute( + "image", + "chrome://global/skin/icons/loading.png" + ); + this.menulist.disabled = true; + this.button.disabled = true; + } + + /** + * Enable the inputs and set a data-l10n-id on the menulist. This can be + * reverted with `disableWithMessageId()`. + */ + enableWithMessageId(messageId) { + this.menulist.setAttribute("data-l10n-id", messageId); + this.menulist.removeAttribute("image"); + this.menulist.disabled = this.menulist.itemCount == 0; + this.button.disabled = !this.menulist.selectedItem; + } +} + +/** + * @typedef LocaleDisplayInfo + * @type {object} + * @property {string} id - A unique ID. + * @property {string} label - The localized display name. + * @property {string} value - The BCP 47 locale identifier or the word "search". + * @property {boolean} canRemove - Locales that are part of the packaged locales cannot be + * removed. + * @property {boolean} installed - Whether or not the locale is installed. + */ + +/** + * @param {Array<string>} localeCodes - List of BCP 47 locale identifiers. + * @returns {Array<LocaleDisplayInfo>} + */ +async function getLocaleDisplayInfo(localeCodes) { + let availableLocales = new Set(await getAvailableLocales()); + let packagedLocales = new Set(Services.locale.packagedLocales); + let localeNames = Services.intl.getLocaleDisplayNames( + undefined, + localeCodes, + { preferNative: true } + ); + return localeCodes.map((code, i) => { + return { + id: "locale-" + code, + label: localeNames[i], + value: code, + canRemove: !packagedLocales.has(code), + installed: availableLocales.has(code), + }; + }); +} + +/** + * @param {LocaleDisplayInfo} a + * @param {LocaleDisplayInfo} b + * @returns {number} + */ +function compareItems(a, b) { + // Sort by installed. + if (a.installed != b.installed) { + return a.installed ? -1 : 1; + + // The search label is always last. + } else if (a.value == "search") { + return 1; + } else if (b.value == "search") { + return -1; + + // If both items are locales, sort by label. + } else if (a.value && b.value) { + return a.label.localeCompare(b.label); + + // One of them is a label, put it first. + } else if (a.value) { + return 1; + } + return -1; +} + +var gMessengerLanguagesDialog = { + /** + * The publicly readable list of selected locales. It is only set when the dialog is + * accepted, and can be retrieved elsewhere by directly reading the property + * on gMessengerLanguagesDialog. + * + * let { selected } = gMessengerLanguagesDialog; + * + * @type {null | Array<string>} + */ + selected: null, + + /** + * @type {SortedItemSelectList} + */ + _availableLocalesUI: null, + + /** + * @type {OrderedListBox} + */ + _selectedLocalesUI: null, + + get downloadEnabled() { + // Downloading langpacks isn't always supported, check the pref. + return Services.prefs.getBoolPref("intl.multilingual.downloadEnabled"); + }, + + async onLoad() { + /** + * @typedef {object} Options - Options passed in to configure the subdialog. + * @property {Array<string>} [selectedLocalesForRestart] The optional list of + * previously selected locales for when a restart is required. This list is + * preserved between openings of the dialog. + * @property {boolean} search Whether the user opened this from "Search for more + * languages" option. + */ + + /** @type {Options} */ + let { selectedLocalesForRestart, search } = window.arguments[0]; + + // This is a list of available locales that the user selected. It's more + // restricted than the Intl notion of `requested` as it only contains + // locale codes for which we have matching locales available. + // The first time this dialog is opened, populate with appLocalesAsBCP47. + let selectedLocales = + selectedLocalesForRestart || Services.locale.appLocalesAsBCP47; + let selectedLocaleSet = new Set(selectedLocales); + let available = await getAvailableLocales(); + let availableSet = new Set(available); + + // Filter selectedLocales since the user may select a locale when it is + // available and then disable it. + selectedLocales = selectedLocales.filter(locale => + availableSet.has(locale) + ); + // Nothing in available should be in selectedSet. + available = available.filter(locale => !selectedLocaleSet.has(locale)); + + await this.initSelectedLocales(selectedLocales); + await this.initAvailableLocales(available, search); + + this.initialized = true; + + // Now the component is initialized, it's safe to accept the results. + document + .getElementById("MessengerLanguagesDialog") + .addEventListener("beforeaccept", () => { + this.selected = this._selectedLocalesUI.items.map(item => item.value); + }); + }, + + /** + * @param {string[]} selectedLocales - BCP 47 locale identifiers + */ + async initSelectedLocales(selectedLocales) { + this._selectedLocalesUI = new OrderedListBox({ + richlistbox: document.getElementById("selectedLocales"), + upButton: document.getElementById("up"), + downButton: document.getElementById("down"), + removeButton: document.getElementById("remove"), + onRemove: item => this.selectedLocaleRemoved(item), + }); + this._selectedLocalesUI.setItems( + await getLocaleDisplayInfo(selectedLocales) + ); + }, + + /** + * @param {Set<string>} available - The set of available BCP 47 locale identifiers. + * @param {boolean} search - Whether the user opened this from "Search for more + * languages" option. + */ + async initAvailableLocales(available, search) { + this._availableLocalesUI = new SortedItemSelectList({ + menulist: document.getElementById("availableLocales"), + button: document.getElementById("add"), + compareFn: compareItems, + onSelect: item => this.availableLanguageSelected(item), + onChange: item => { + this.hideError(); + if (item.value == "search") { + this.loadLocalesFromAMO(); + } + }, + }); + + // Populate the list with the installed locales even if the user is + // searching in case the download fails. + await this.loadLocalesFromInstalled(available); + + // If the user opened this from the "Search for more languages" option, + // search AMO for available locales. + if (search) { + return this.loadLocalesFromAMO(); + } + + return undefined; + }, + + async loadLocalesFromAMO() { + if (!this.downloadEnabled) { + return; + } + + // Disable the dropdown while we hit the network. + this._availableLocalesUI.disableWithMessageId( + "messenger-languages-searching" + ); + + // Fetch the available langpacks from AMO. + let availableLangpacks; + try { + availableLangpacks = await AddonRepository.getAvailableLangpacks(); + } catch (e) { + this.showError(); + return; + } + + // Store the available langpack info for later use. + this.availableLangpacks = new Map(); + for (let { target_locale, url, hash } of availableLangpacks) { + this.availableLangpacks.set(target_locale, { url, hash }); + } + + // Remove the installed locales from the available ones. + let installedLocales = new Set(await getAvailableLocales()); + let notInstalledLocales = availableLangpacks + .filter(({ target_locale }) => !installedLocales.has(target_locale)) + .map(lang => lang.target_locale); + + // Create the rows for the remote locales. + let availableItems = await getLocaleDisplayInfo(notInstalledLocales); + availableItems.push({ + label: await document.l10n.formatValue( + "messenger-languages-available-label" + ), + className: "label-item", + disabled: true, + installed: false, + }); + + // Remove the search option and add the remote locales. + let items = this._availableLocalesUI.items; + items.pop(); + items = items.concat(availableItems); + + // Update the dropdown and enable it again. + this._availableLocalesUI.setItems(items); + this._availableLocalesUI.enableWithMessageId( + "messenger-languages-select-language" + ); + }, + + /** + * @param {Set<string>} available - The set of available (BCP 47) locales. + */ + async loadLocalesFromInstalled(available) { + let items; + if (available.length > 0) { + items = await getLocaleDisplayInfo(available); + items.push(await this.createInstalledLabel()); + } else { + items = []; + } + if (this.downloadEnabled) { + items.push({ + label: await document.l10n.formatValue("messenger-languages-search"), + value: "search", + }); + } + this._availableLocalesUI.setItems(items); + }, + + /** + * @param {LocaleDisplayInfo} item + */ + async availableLanguageSelected(item) { + if ((await getAvailableLocales()).includes(item.value)) { + await this.requestLocalLanguage(item); + } else if (this.availableLangpacks.has(item.value)) { + await this.requestRemoteLanguage(item); + } else { + this.showError(); + } + }, + + /** + * @param {LocaleDisplayInfo} item + */ + async requestLocalLanguage(item) { + this._selectedLocalesUI.addItem(item); + let selectedCount = this._selectedLocalesUI.items.length; + let availableCount = (await getAvailableLocales()).length; + if (selectedCount == availableCount) { + // Remove the installed label, they're all installed. + this._availableLocalesUI.items.shift(); + this._availableLocalesUI.setItems(this._availableLocalesUI.items); + } + + // The label isn't always reset when the selected item is removed, so set it again. + this._availableLocalesUI.enableWithMessageId( + "messenger-languages-select-language" + ); + }, + + /** + * @param {LocaleDisplayInfo} item + */ + async requestRemoteLanguage(item) { + this._availableLocalesUI.disableWithMessageId( + "messenger-languages-downloading" + ); + + let { url, hash } = this.availableLangpacks.get(item.value); + let addon; + + try { + addon = await AddonManager.getInstallForURL(url, { hash }); + await addon.install(); + } catch (e) { + this.showError(); + return; + } + + // If the add-on was previously installed, it might be disabled still. + if (addon.userDisabled) { + await addon.enable(); + } + + item.installed = true; + this._selectedLocalesUI.addItem(item); + this._availableLocalesUI.enableWithMessageId( + "messenger-languages-select-language" + ); + }, + + showError() { + document.getElementById("warning-message").hidden = false; + this._availableLocalesUI.enableWithMessageId( + "messenger-languages-select-language" + ); + + // The height has likely changed, find our SubDialog and tell it to resize. + requestAnimationFrame(() => { + let dialogs = window.opener.gSubDialog._dialogs; + let index = dialogs.findIndex(d => d._frame.contentDocument == document); + if (index != -1) { + dialogs[index].resizeDialog(); + } + }); + }, + + hideError() { + document.getElementById("warning-message").hidden = true; + }, + + /** + * @param {LocaleDisplayInfo} item + */ + async selectedLocaleRemoved(item) { + this._availableLocalesUI.addItem(item); + + // If the item we added is at the top of the list, it needs the label. + if (this._availableLocalesUI.items[0] == item) { + this._availableLocalesUI.addItem(await this.createInstalledLabel()); + } + }, + + async createInstalledLabel() { + return { + label: await document.l10n.formatValue( + "messenger-languages-installed-label" + ), + className: "label-item", + disabled: true, + installed: true, + }; + }, +}; diff --git a/comm/mail/components/preferences/messengerLanguages.xhtml b/comm/mail/components/preferences/messengerLanguages.xhtml new file mode 100644 index 0000000000..116ee4e5b2 --- /dev/null +++ b/comm/mail/components/preferences/messengerLanguages.xhtml @@ -0,0 +1,93 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="messenger-languages-window2" + onload="gMessengerLanguagesDialog.onLoad();" +> + <dialog id="MessengerLanguagesDialog" buttons="accept,cancel"> + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link + rel="localization" + href="messenger/preferences/languages.ftl" + /> + </linkset> + + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://messenger/content/preferences/messengerLanguages.js" /> + + <vbox + id="messengerLanguagesDialogPane" + class="prefpane largeDialogContainer" + flex="1" + > + <description data-l10n-id="messenger-languages-description" /> + <hbox flex="1"> + <vbox flex="1"> + <richlistbox id="selectedLocales" flex="1" /> + <menulist + id="availableLocales" + class="available-locales-list" + data-l10n-id="messenger-languages-select-language" + data-l10n-attrs="placeholder,label" + > + <menupopup /> + </menulist> + </vbox> + <vbox> + <button + id="up" + class="action-button" + disabled="true" + data-l10n-id="languages-customize-moveup" + /> + <button + id="down" + class="action-button" + disabled="true" + data-l10n-id="languages-customize-movedown" + /> + <button + id="remove" + class="action-button" + disabled="true" + data-l10n-id="languages-customize-remove" + /> + <vbox flex="1" pack="end"> + <button + id="add" + class="add-messenger-language action-button" + data-l10n-id="languages-customize-add" + disabled="true" + /> + </vbox> + </vbox> + </hbox> + <hbox + id="warning-message" + class="message-bar message-bar-warning" + hidden="true" + > + <html:img + class="message-bar-icon" + src="chrome://global/skin/icons/warning.svg" + alt="" + /> + <description + class="message-bar-description" + data-l10n-id="messenger-languages-error" + /> + </hbox> + <separator class="thin" /> + </vbox> + </dialog> +</window> diff --git a/comm/mail/components/preferences/moz.build b/comm/mail/components/preferences/moz.build new file mode 100644 index 0000000000..4eff028aeb --- /dev/null +++ b/comm/mail/components/preferences/moz.build @@ -0,0 +1,18 @@ +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ["jar.mn"] + +DEFINES["MOZ_MACBUNDLE_NAME"] = CONFIG["MOZ_MACBUNDLE_NAME"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("windows", "gtk", "cocoa"): + DEFINES["HAVE_SHELL_SERVICE"] = 1 + +if CONFIG["MOZ_UPDATER"]: + DEFINES["MOZ_UPDATER"] = 1 + +BROWSER_CHROME_MANIFESTS += [ + "test/browser/browser.ini", +] diff --git a/comm/mail/components/preferences/notifications.js b/comm/mail/components/preferences/notifications.js new file mode 100644 index 0000000000..0970a944bd --- /dev/null +++ b/comm/mail/components/preferences/notifications.js @@ -0,0 +1,25 @@ +/* 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 ../../../../toolkit/content/preferencesBindings.js */ + +Preferences.addAll([ + { id: "mail.biff.alert.show_preview", type: "bool" }, + { id: "mail.biff.alert.show_subject", type: "bool" }, + { id: "mail.biff.alert.show_sender", type: "bool" }, + { id: "alerts.totalOpenTime", type: "int" }, +]); + +var gNotificationsDialog = { + init() { + let element = document.getElementById("totalOpenTime"); + Preferences.addSyncFromPrefListener( + element, + () => Preferences.get("alerts.totalOpenTime").value / 1000 + ); + Preferences.addSyncToPrefListener(element, element => element.value * 1000); + }, +}; + +window.addEventListener("load", () => gNotificationsDialog.init()); diff --git a/comm/mail/components/preferences/notifications.xhtml b/comm/mail/components/preferences/notifications.xhtml new file mode 100644 index 0000000000..d9abb47e4e --- /dev/null +++ b/comm/mail/components/preferences/notifications.xhtml @@ -0,0 +1,71 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<!DOCTYPE window> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="notifications-dialog-window" +> + <dialog id="NotificationsDialog" dlgbuttons="accept,cancel"> + <linkset> + <html:link + rel="localization" + href="messenger/preferences/notifications.ftl" + /> + </linkset> + <description data-l10n-id="customize-alert-description" /> + + <checkbox + id="previewText" + class="indent" + data-l10n-id="preview-text-checkbox" + preference="mail.biff.alert.show_preview" + /> + <checkbox + id="subject" + class="indent" + data-l10n-id="subject-checkbox" + preference="mail.biff.alert.show_subject" + /> + <checkbox + id="sender" + class="indent" + data-l10n-id="sender-checkbox" + preference="mail.biff.alert.show_sender" + /> + + <separator /> + + <hbox align="center"> + <label + id="totalOpenTimeBefore" + control="totalOpenTime" + data-l10n-id="open-time-label-before" + /> + <html:input + id="totalOpenTime" + type="number" + class="size3" + min="1" + max="3600" + preference="alerts.totalOpenTime" + /> + <label + id="totalOpenTimeEnd" + data-l10n-id="open-time-label-after" + class="startSpacing" + /> + </hbox> + <separator /> + + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://messenger/content/preferences/notifications.js" /> + </dialog> +</window> diff --git a/comm/mail/components/preferences/offline.js b/comm/mail/components/preferences/offline.js new file mode 100644 index 0000000000..ae33950cf1 --- /dev/null +++ b/comm/mail/components/preferences/offline.js @@ -0,0 +1,31 @@ +/* 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 ../../../../toolkit/content/preferencesBindings.js */ + +Preferences.addAll([ + { id: "offline.autoDetect", type: "bool" }, + { id: "offline.startup_state", type: "int" }, + { id: "offline.send.unsent_messages", type: "int" }, + { id: "offline.download.download_messages", type: "int" }, +]); + +var kAutomatic = 4; +var kRememberLastState = 0; + +var gOfflineDialog = { + dialogSetup() { + let offlineAutoDetection = Preferences.get("offline.autoDetect"); + let offlineStartupStatePref = Preferences.get("offline.startup_state"); + + offlineStartupStatePref.disabled = offlineAutoDetection.value; + if (offlineStartupStatePref.disabled) { + offlineStartupStatePref.value = kAutomatic; + } else if (offlineStartupStatePref.value == kAutomatic) { + offlineStartupStatePref.value = kRememberLastState; + } + }, +}; + +Preferences.get("offline.autoDetect").on("change", gOfflineDialog.dialogSetup); diff --git a/comm/mail/components/preferences/offline.xhtml b/comm/mail/components/preferences/offline.xhtml new file mode 100644 index 0000000000..77a86cfa86 --- /dev/null +++ b/comm/mail/components/preferences/offline.xhtml @@ -0,0 +1,77 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<!DOCTYPE window> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gOfflineDialog.dialogSetup();" + data-l10n-id="offline-dialog-window" +> + <dialog id="OfflineSettingsDialog" dlgbuttons="accept,cancel"> + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://messenger/content/preferences/offline.js" /> + + <linkset> + <html:link rel="localization" href="messenger/preferences/offline.ftl" /> + </linkset> + + <checkbox + data-l10n-id="autodetect-online-label" + preference="offline.autoDetect" + /> + + <separator class="thin" /> + + <label + data-l10n-id="offline-preference-startup-label" + control="whenStartingUp" + /> + <radiogroup + id="whenStartingUp" + class="indent" + preference="offline.startup_state" + > + <radio value="0" data-l10n-id="status-radio-remember" /> + <radio value="1" data-l10n-id="status-radio-ask" /> + <radio value="2" data-l10n-id="status-radio-always-online" /> + <radio value="3" data-l10n-id="status-radio-always-offline" /> + <radio value="4" hidden="true" /> + </radiogroup> + + <separator /> + + <label data-l10n-id="going-online-label" control="whengoingOnlinestate" /> + <radiogroup + id="whengoingOnlinestate" + orient="horizontal" + class="indent" + preference="offline.send.unsent_messages" + > + <radio value="1" data-l10n-id="going-online-auto" /> + <radio value="2" data-l10n-id="going-online-not" /> + <radio value="0" data-l10n-id="going-online-ask" /> + </radiogroup> + + <separator class="thin" /> + + <label data-l10n-id="going-offline-label" control="whengoingOfflinestate" /> + <radiogroup + id="whengoingOfflinestate" + orient="horizontal" + class="indent" + preference="offline.download.download_messages" + > + <radio value="1" data-l10n-id="going-offline-auto" /> + <radio value="2" data-l10n-id="going-offline-not" /> + <radio value="0" data-l10n-id="going-offline-ask" /> + </radiogroup> + <separator /> + </dialog> +</window> diff --git a/comm/mail/components/preferences/passwordManager.js b/comm/mail/components/preferences/passwordManager.js new file mode 100644 index 0000000000..67f08767d3 --- /dev/null +++ b/comm/mail/components/preferences/passwordManager.js @@ -0,0 +1,819 @@ +/* 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/. */ + +/** * =================== SAVED SIGNONS CODE =================== */ +/* eslint-disable-next-line no-var */ +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +/* eslint-disable-next-line no-var */ +/* eslint-disable-next-line no-var */ +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +// Default value for signon table sorting +let lastSignonSortColumn = "origin"; +let lastSignonSortAscending = true; + +let showingPasswords = false; + +// password-manager lists +let signons = []; +let deletedSignons = []; + +// Elements that would be used frequently +let filterField; +let togglePasswordsButton; +let signonsIntro; +let removeButton; +let removeAllButton; +let signonsTree; + +let signonReloadDisplay = { + observe(subject, topic, data) { + if (topic == "passwordmgr-storage-changed") { + switch (data) { + case "addLogin": + case "modifyLogin": + case "removeLogin": + case "removeAllLogins": + if (!signonsTree) { + return; + } + signons.length = 0; + LoadSignons(); + // apply the filter if needed + if (filterField && filterField.value != "") { + FilterPasswords(); + } + signonsTree.ensureRowIsVisible( + signonsTree.view.selection.currentIndex + ); + break; + } + Services.obs.notifyObservers(null, "passwordmgr-dialog-updated"); + } + }, +}; + +// Formatter for localization. +let dateFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "medium", +}); +let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "medium", + timeStyle: "short", +}); + +function Startup() { + // be prepared to reload the display if anything changes + Services.obs.addObserver(signonReloadDisplay, "passwordmgr-storage-changed"); + + signonsTree = document.getElementById("signonsTree"); + filterField = document.getElementById("filter"); + togglePasswordsButton = document.getElementById("togglePasswords"); + signonsIntro = document.getElementById("signonsIntro"); + removeButton = document.getElementById("removeSignon"); + removeAllButton = document.getElementById("removeAllSignons"); + + document.l10n.setAttributes(togglePasswordsButton, "show-passwords"); + document.l10n.setAttributes(signonsIntro, "logins-description-all"); + document.l10n.setAttributes(removeAllButton, "remove-all"); + + document + .getElementsByTagName("treecols")[0] + .addEventListener("click", event => { + let { target, button } = event; + let sortField = target.getAttribute("data-field-name"); + + if (target.nodeName != "treecol" || button != 0 || !sortField) { + return; + } + + SignonColumnSort(sortField); + }); + + LoadSignons(); + + // filter the table if requested by caller + if ( + window.arguments && + window.arguments[0] && + window.arguments[0].filterString + ) { + setFilter(window.arguments[0].filterString); + } + + FocusFilterBox(); + document.l10n + .translateElements(document.querySelectorAll("[data-l10n-id]")) + .then(() => window.sizeToContent()); +} + +function Shutdown() { + Services.obs.removeObserver( + signonReloadDisplay, + "passwordmgr-storage-changed" + ); +} + +function setFilter(aFilterString) { + filterField.value = aFilterString; + FilterPasswords(); +} + +let signonsTreeView = { + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), + _filterSet: [], + _lastSelectedRanges: [], + selection: null, + + rowCount: 0, + setTree(tree) {}, + getImageSrc(row, column) { + if (column.element.getAttribute("id") !== "providerCol") { + return ""; + } + + const signon = GetVisibleLogins()[row]; + + return PlacesUtils.urlWithSizeRef(window, "page-icon:" + signon.origin, 16); + }, + getCellValue(row, column) {}, + getCellText(row, column) { + let time; + let signon = GetVisibleLogins()[row]; + switch (column.id) { + case "providerCol": + return signon.httpRealm + ? signon.origin + " (" + signon.httpRealm + ")" + : signon.origin; + case "userCol": + return signon.username || ""; + case "passwordCol": + return signon.password || ""; + case "timeCreatedCol": + time = new Date(signon.timeCreated); + return dateFormatter.format(time); + case "timeLastUsedCol": + time = new Date(signon.timeLastUsed); + return dateAndTimeFormatter.format(time); + case "timePasswordChangedCol": + time = new Date(signon.timePasswordChanged); + return dateFormatter.format(time); + case "timesUsedCol": + return signon.timesUsed; + default: + return ""; + } + }, + isEditable(row, col) { + if (col.id == "userCol" || col.id == "passwordCol") { + return true; + } + return false; + }, + isSeparator(index) { + return false; + }, + isSorted() { + return false; + }, + isContainer(index) { + return false; + }, + cycleHeader(column) {}, + getRowProperties(row) { + return ""; + }, + getColumnProperties(column) { + return ""; + }, + getCellProperties(row, column) { + if (column.element.getAttribute("id") == "providerCol") { + return "ltr"; + } + + return ""; + }, + setCellText(row, col, value) { + let table = GetVisibleLogins(); + function _editLogin(field) { + if (value == table[row][field]) { + return; + } + let existingLogin = table[row].clone(); + table[row][field] = value; + table[row].timePasswordChanged = Date.now(); + Services.logins.modifyLogin(existingLogin, table[row]); + signonsTree.invalidateRow(row); + } + + if (col.id == "userCol") { + _editLogin("username"); + } else if (col.id == "passwordCol") { + if (!value) { + return; + } + _editLogin("password"); + } + }, +}; + +function SortTree(column, ascending) { + let table = GetVisibleLogins(); + // remember which item was selected so we can restore it after the sort + let selections = GetTreeSelections(); + let selectedNumber = selections.length ? table[selections[0]].number : -1; + function compareFunc(a, b) { + let valA, valB; + switch (column) { + case "origin": + let realmA = a.httpRealm; + let realmB = b.httpRealm; + realmA = realmA == null ? "" : realmA.toLowerCase(); + realmB = realmB == null ? "" : realmB.toLowerCase(); + + valA = a[column].toLowerCase() + realmA; + valB = b[column].toLowerCase() + realmB; + break; + case "username": + case "password": + valA = a[column].toLowerCase(); + valB = b[column].toLowerCase(); + break; + + default: + valA = a[column]; + valB = b[column]; + } + + if (valA < valB) { + return -1; + } + if (valA > valB) { + return 1; + } + return 0; + } + + // do the sort + table.sort(compareFunc); + if (!ascending) { + table.reverse(); + } + + // restore the selection + let selectedRow = -1; + if (selectedNumber >= 0 && false) { + for (let s = 0; s < table.length; s++) { + if (table[s].number == selectedNumber) { + // update selection + // note: we need to deselect before reselecting in order to trigger ...Selected() + signonsTree.view.selection.select(-1); + signonsTree.view.selection.select(s); + selectedRow = s; + break; + } + } + } + + // display the results + signonsTree.invalidate(); + if (selectedRow >= 0) { + signonsTree.ensureRowIsVisible(selectedRow); + } +} + +function LoadSignons() { + // loads signons into table + try { + signons = Services.logins.getAllLogins(); + } catch (e) { + signons = []; + } + signons.forEach(login => login.QueryInterface(Ci.nsILoginMetaInfo)); + signonsTreeView.rowCount = signons.length; + + // sort and display the table + signonsTree.view = signonsTreeView; + // The sort column didn't change. SortTree (called by + // SignonColumnSort) assumes we want to toggle the sort + // direction but here we don't so we have to trick it + lastSignonSortAscending = !lastSignonSortAscending; + SignonColumnSort(lastSignonSortColumn); + + // disable "remove all signons" button if there are no signons + if (signons.length == 0) { + removeAllButton.setAttribute("disabled", "true"); + togglePasswordsButton.setAttribute("disabled", "true"); + } else { + removeAllButton.removeAttribute("disabled"); + togglePasswordsButton.removeAttribute("disabled"); + } + + return true; +} + +function GetVisibleLogins() { + return signonsTreeView._filterSet.length + ? signonsTreeView._filterSet + : signons; +} + +function GetTreeSelections() { + let selections = []; + let select = signonsTree.view.selection; + if (select) { + let count = select.getRangeCount(); + let min = {}; + let max = {}; + for (let i = 0; i < count; i++) { + select.getRangeAt(i, min, max); + for (let k = min.value; k <= max.value; k++) { + if (k != -1) { + selections[selections.length] = k; + } + } + } + } + return selections; +} + +function SignonSelected() { + let selections = GetTreeSelections(); + if (selections.length) { + removeButton.removeAttribute("disabled"); + } else { + removeButton.setAttribute("disabled", true); + } +} + +function DeleteSignon() { + let syncNeeded = signonsTreeView._filterSet.length != 0; + let tree = signonsTree; + let view = signonsTreeView; + let table = GetVisibleLogins(); + + // Turn off tree selection notifications during the deletion + tree.view.selection.selectEventsSuppressed = true; + + // remove selected items from list (by setting them to null) and place in deleted list + let selections = GetTreeSelections(); + for (let s = selections.length - 1; s >= 0; s--) { + let i = selections[s]; + deletedSignons.push(table[i]); + table[i] = null; + } + + // collapse list by removing all the null entries + for (let j = 0; j < table.length; j++) { + if (table[j] == null) { + let k = j; + while (k < table.length && table[k] == null) { + k++; + } + table.splice(j, k - j); + view.rowCount -= k - j; + tree.rowCountChanged(j, j - k); + } + } + + // update selection and/or buttons + if (table.length) { + // update selection + let nextSelection = + selections[0] < table.length ? selections[0] : table.length - 1; + tree.view.selection.select(nextSelection); + } else { + // disable buttons + removeButton.setAttribute("disabled", "true"); + removeAllButton.setAttribute("disabled", "true"); + } + tree.view.selection.selectEventsSuppressed = false; + FinalizeSignonDeletions(syncNeeded); +} + +async function DeleteAllSignons() { + // Confirm the user wants to remove all passwords + let dummy = { value: false }; + let [title, message] = await document.l10n.formatValues([ + { id: "remove-all-passwords-title" }, + { id: "remove-all-passwords-prompt" }, + ]); + if ( + Services.prompt.confirmEx( + window, + title, + message, + Services.prompt.STD_YES_NO_BUTTONS + Services.prompt.BUTTON_POS_1_DEFAULT, + null, + null, + null, + null, + dummy + ) == 1 + ) { + // 1 == "No" button + return; + } + + let syncNeeded = signonsTreeView._filterSet.length != 0; + let view = signonsTreeView; + let table = GetVisibleLogins(); + + // remove all items from table and place in deleted table + for (let i = 0; i < table.length; i++) { + deletedSignons.push(table[i]); + } + table.length = 0; + + // clear out selections + view.selection.select(-1); + + // update the tree view and notify the tree + view.rowCount = 0; + + signonsTree.rowCountChanged(0, -deletedSignons.length); + signonsTree.invalidate(); + + // disable buttons + removeButton.setAttribute("disabled", "true"); + removeAllButton.setAttribute("disabled", "true"); + FinalizeSignonDeletions(syncNeeded); +} + +async function TogglePasswordVisible() { + if (showingPasswords || (await masterPasswordLogin(AskUserShowPasswords))) { + showingPasswords = !showingPasswords; + document.l10n.setAttributes( + togglePasswordsButton, + showingPasswords ? "hide-passwords" : "show-passwords" + ); + document.getElementById("passwordCol").hidden = !showingPasswords; + FilterPasswords(); + } + + // Notify observers that the password visibility toggling is + // completed. (Mostly useful for tests) + Services.obs.notifyObservers(null, "passwordmgr-password-toggle-complete"); +} + +async function AskUserShowPasswords() { + let dummy = { value: false }; + + // Confirm the user wants to display passwords + return ( + Services.prompt.confirmEx( + window, + null, + await document.l10n.formatValue("no-master-password-prompt"), + Services.prompt.STD_YES_NO_BUTTONS, + null, + null, + null, + null, + dummy + ) == 0 + ); // 0=="Yes" button +} + +function FinalizeSignonDeletions(syncNeeded) { + for (let s = 0; s < deletedSignons.length; s++) { + Services.logins.removeLogin(deletedSignons[s]); + } + // If the deletion has been performed in a filtered view, reflect the deletion in the unfiltered table. + // See bug 405389. + if (syncNeeded) { + try { + signons = Services.logins.getAllLogins(); + } catch (e) { + signons = []; + } + } + deletedSignons.length = 0; +} + +function HandleSignonKeyPress(e) { + // If editing is currently performed, don't do anything. + if (signonsTree.getAttribute("editing")) { + return; + } + if ( + e.keyCode == KeyboardEvent.DOM_VK_DELETE || + (AppConstants.platform == "macosx" && + e.keyCode == KeyboardEvent.DOM_VK_BACK_SPACE) + ) { + DeleteSignon(); + e.preventDefault(); + } +} + +function getColumnByName(column) { + switch (column) { + case "origin": + return document.getElementById("providerCol"); + case "username": + return document.getElementById("userCol"); + case "password": + return document.getElementById("passwordCol"); + case "timeCreated": + return document.getElementById("timeCreatedCol"); + case "timeLastUsed": + return document.getElementById("timeLastUsedCol"); + case "timePasswordChanged": + return document.getElementById("timePasswordChangedCol"); + case "timesUsed": + return document.getElementById("timesUsedCol"); + } + return undefined; +} + +function SignonColumnSort(column) { + let sortedCol = getColumnByName(column); + let lastSortedCol = getColumnByName(lastSignonSortColumn); + + // clear out the sortDirection attribute on the old column + lastSortedCol.removeAttribute("sortDirection"); + + // determine if sort is to be ascending or descending + lastSignonSortAscending = + column == lastSignonSortColumn ? !lastSignonSortAscending : true; + + // sort + lastSignonSortColumn = column; + SortTree(lastSignonSortColumn, lastSignonSortAscending); + + // set the sortDirection attribute to get the styling going + // first we need to get the right element + sortedCol.setAttribute( + "sortDirection", + lastSignonSortAscending ? "ascending" : "descending" + ); +} + +function SignonClearFilter() { + let singleSelection = signonsTreeView.selection.count == 1; + + // Clear the Tree Display + signonsTreeView.rowCount = 0; + signonsTree.rowCountChanged(0, -signonsTreeView._filterSet.length); + signonsTreeView._filterSet = []; + + // Just reload the list to make sure deletions are respected + LoadSignons(); + + // Restore selection + if (singleSelection) { + signonsTreeView.selection.clearSelection(); + for (let i = 0; i < signonsTreeView._lastSelectedRanges.length; ++i) { + let range = signonsTreeView._lastSelectedRanges[i]; + signonsTreeView.selection.rangedSelect(range.min, range.max, true); + } + } else { + signonsTreeView.selection.select(0); + } + signonsTreeView._lastSelectedRanges = []; + + document.l10n.setAttributes(signonsIntro, "logins-description-all"); + document.l10n.setAttributes(removeAllButton, "remove-all"); +} + +function FocusFilterBox() { + if (filterField.getAttribute("focused") != "true") { + filterField.focus(); + } +} + +function SignonMatchesFilter(aSignon, aFilterValue) { + if (aSignon.origin.toLowerCase().includes(aFilterValue)) { + return true; + } + if ( + aSignon.username && + aSignon.username.toLowerCase().includes(aFilterValue) + ) { + return true; + } + if ( + aSignon.httpRealm && + aSignon.httpRealm.toLowerCase().includes(aFilterValue) + ) { + return true; + } + if ( + showingPasswords && + aSignon.password && + aSignon.password.toLowerCase().includes(aFilterValue) + ) { + return true; + } + + return false; +} + +function _filterPasswords(aFilterValue, view) { + aFilterValue = aFilterValue.toLowerCase(); + return signons.filter(s => SignonMatchesFilter(s, aFilterValue)); +} + +function SignonSaveState() { + // Save selection + let seln = signonsTreeView.selection; + signonsTreeView._lastSelectedRanges = []; + let rangeCount = seln.getRangeCount(); + for (let i = 0; i < rangeCount; ++i) { + let min = {}; + let max = {}; + seln.getRangeAt(i, min, max); + signonsTreeView._lastSelectedRanges.push({ + min: min.value, + max: max.value, + }); + } +} + +function FilterPasswords() { + if (filterField.value == "") { + SignonClearFilter(); + return; + } + + let newFilterSet = _filterPasswords(filterField.value, signonsTreeView); + if (!signonsTreeView._filterSet.length) { + // Save Display Info for the Non-Filtered mode when we first + // enter Filtered mode. + SignonSaveState(); + } + signonsTreeView._filterSet = newFilterSet; + + // Clear the display + let oldRowCount = signonsTreeView.rowCount; + signonsTreeView.rowCount = 0; + signonsTree.rowCountChanged(0, -oldRowCount); + // Set up the filtered display + signonsTreeView.rowCount = signonsTreeView._filterSet.length; + signonsTree.rowCountChanged(0, signonsTreeView.rowCount); + + // if the view is not empty then select the first item + if (signonsTreeView.rowCount > 0) { + signonsTreeView.selection.select(0); + } + + document.l10n.setAttributes(signonsIntro, "logins-description-filtered"); + document.l10n.setAttributes(removeAllButton, "remove-all-shown"); +} + +function CopyProviderUrl() { + // Copy selected provider url to clipboard + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + let row = signonsTree.currentIndex; + let url = signonsTreeView.getCellText(row, { id: "providerCol" }); + clipboard.copyString(url); +} + +async function CopyPassword() { + // Don't copy passwords if we aren't already showing the passwords & a master + // password hasn't been entered. + if (!showingPasswords && !(await masterPasswordLogin())) { + return; + } + // Copy selected signon's password to clipboard + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + let row = signonsTree.currentIndex; + let password = signonsTreeView.getCellText(row, { id: "passwordCol" }); + clipboard.copyString(password); +} + +function CopyUsername() { + // Copy selected signon's username to clipboard + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper + ); + let row = signonsTree.currentIndex; + let username = signonsTreeView.getCellText(row, { id: "userCol" }); + clipboard.copyString(username); +} + +function EditCellInSelectedRow(columnName) { + let row = signonsTree.currentIndex; + let columnElement = getColumnByName(columnName); + signonsTree.startEditing( + row, + signonsTree.columns.getColumnFor(columnElement) + ); +} + +function UpdateContextMenu() { + let singleSelection = signonsTreeView.selection.count == 1; + let menuItems = new Map(); + let menupopup = document.getElementById("signonsTreeContextMenu"); + for (let menuItem of menupopup.querySelectorAll("menuitem")) { + menuItems.set(menuItem.id, menuItem); + } + + if (!singleSelection) { + for (let menuItem of menuItems.values()) { + menuItem.setAttribute("disabled", "true"); + } + return; + } + + let selectedRow = signonsTree.currentIndex; + + // Disable "Copy Username" if the username is empty. + if (signonsTreeView.getCellText(selectedRow, { id: "userCol" }) != "") { + menuItems.get("context-copyusername").removeAttribute("disabled"); + } else { + menuItems.get("context-copyusername").setAttribute("disabled", "true"); + } + + menuItems.get("context-copyproviderurl").removeAttribute("disabled"); + menuItems.get("context-editusername").removeAttribute("disabled"); + menuItems.get("context-copypassword").removeAttribute("disabled"); + + // Disable "Edit Password" if the password column isn't showing. + if (!document.getElementById("passwordCol").hidden) { + menuItems.get("context-editpassword").removeAttribute("disabled"); + } else { + menuItems.get("context-editpassword").setAttribute("disabled", "true"); + } +} + +async function masterPasswordLogin(noPasswordCallback) { + // This doesn't harm if passwords are not encrypted + let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance( + Ci.nsIPK11TokenDB + ); + let token = tokendb.getInternalKeyToken(); + + // If there is no primary password, still give the user a chance to opt-out of displaying passwords + if (token.checkPassword("")) { + // The OS re-authentication on Linux isn't working (Bug 1527745), + // still add the confirm dialog for Linux. + if ( + Services.prefs.getBoolPref("signon.management.page.os-auth.enabled") && + AppConstants.platform !== "linux" + ) { + // Require OS authentication before the user can show the passwords or copy them. + let messageId = "password-os-auth-dialog-message"; + if (AppConstants.platform == "macosx") { + // MacOS requires a special format of this dialog string. + // See preferences.ftl for more information. + messageId += "-macosx"; + } + let [messageText, captionText] = await document.l10n.formatMessages([ + { + id: messageId, + }, + { + id: "password-os-auth-dialog-caption", + }, + ]); + let win = Services.wm.getMostRecentWindow(""); + let loggedIn = await OSKeyStore.ensureLoggedIn( + messageText.value, + captionText.value, + win, + false + ); + if (!loggedIn.authenticated) { + return false; + } + return true; + } + return noPasswordCallback ? noPasswordCallback() : true; + } + + // So there's a primary password. But since checkPassword didn't succeed, we're logged out (per nsIPK11Token.idl). + try { + // Relogin and ask for the primary password. + token.login(true); // 'true' means always prompt for token password. User will be prompted until + // clicking 'Cancel' or entering the correct password. + } catch (e) { + // An exception will be thrown if the user cancels the login prompt dialog. + // User is also logged out of Software Security Device. + } + + return token.isLoggedIn(); +} + +function escapeKeyHandler() { + // If editing is currently performed, don't do anything. + if (signonsTree.getAttribute("editing")) { + return; + } + window.close(); +} diff --git a/comm/mail/components/preferences/passwordManager.xhtml b/comm/mail/components/preferences/passwordManager.xhtml new file mode 100644 index 0000000000..594f2da8d8 --- /dev/null +++ b/comm/mail/components/preferences/passwordManager.xhtml @@ -0,0 +1,186 @@ +<?xml version="1.0"?> +<!-- 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 https://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/preferences/passwordmgr.css"?> + +<window + id="SignonViewerDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="Startup();" + onunload="Shutdown();" + data-l10n-id="saved-logins" + persist="width height" +> + <linkset> + <html:link rel="localization" href="branding/brand.ftl" /> + <html:link + rel="localization" + href="messenger/preferences/passwordManager.ftl" + /> + </linkset> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://messenger/content/preferences/passwordManager.js" /> + + <keyset> + <key keycode="VK_ESCAPE" oncommand="escapeKeyHandler();" /> + <key + data-l10n-id="window-close" + modifiers="accel" + oncommand="escapeKeyHandler();" + /> + <key + data-l10n-id="focus-search-primary-shortcut" + modifiers="accel" + oncommand="FocusFilterBox();" + /> + <key + data-l10n-id="focus-search-alt-shortcut" + modifiers="accel" + oncommand="FocusFilterBox();" + /> + </keyset> + + <popupset id="signonsTreeContextSet"> + <menupopup id="signonsTreeContextMenu" onpopupshowing="UpdateContextMenu()"> + <menuitem + id="context-copyproviderurl" + data-l10n-id="copy-provider-url-cmd" + oncommand="CopyProviderUrl()" + /> + <menuseparator /> + <menuitem + id="context-copyusername" + data-l10n-id="copy-username-cmd" + oncommand="CopyUsername()" + /> + <menuitem + id="context-editusername" + data-l10n-id="edit-username-cmd" + oncommand="EditCellInSelectedRow('username')" + /> + <menuseparator /> + <menuitem + id="context-copypassword" + data-l10n-id="copy-password-cmd" + oncommand="CopyPassword()" + /> + <menuitem + id="context-editpassword" + data-l10n-id="edit-password-cmd" + oncommand="EditCellInSelectedRow('password')" + /> + </menupopup> + </popupset> + + <!-- saved signons --> + <vbox id="savedsignons" class="contentPane" flex="1"> + <!-- filter --> + <hbox align="center"> + <search-textbox + id="filter" + flex="1" + aria-controls="signonsTree" + oncommand="FilterPasswords();" + data-l10n-id="search-filter" + /> + </hbox> + + <label control="signonsTree" id="signonsIntro" /> + <separator class="thin" /> + <tree + id="signonsTree" + flex="1" + onkeypress="HandleSignonKeyPress(event)" + onselect="SignonSelected();" + editable="true" + context="signonsTreeContextMenu" + > + <treecols> + <treecol + id="providerCol" + data-l10n-id="column-heading-provider" + data-field-name="origin" + persist="width" + ignoreincolumnpicker="true" + sortDirection="ascending" + /> + <splitter class="tree-splitter" /> + <treecol + id="userCol" + data-l10n-id="column-heading-username" + ignoreincolumnpicker="true" + data-field-name="username" + persist="width" + /> + <splitter class="tree-splitter" /> + <treecol + id="passwordCol" + data-l10n-id="column-heading-password" + ignoreincolumnpicker="true" + data-field-name="password" + persist="width" + hidden="true" + /> + <splitter class="tree-splitter" /> + <treecol + id="timeCreatedCol" + data-l10n-id="column-heading-time-created" + data-field-name="timeCreated" + persist="width hidden" + hidden="true" + /> + <splitter class="tree-splitter" /> + <treecol + id="timeLastUsedCol" + data-l10n-id="column-heading-time-last-used" + data-field-name="timeLastUsed" + persist="width hidden" + hidden="true" + /> + <splitter class="tree-splitter" /> + <treecol + id="timePasswordChangedCol" + data-l10n-id="column-heading-time-password-changed" + data-field-name="timePasswordChanged" + persist="width hidden" + /> + <splitter class="tree-splitter" /> + <treecol + id="timesUsedCol" + data-l10n-id="column-heading-times-used" + data-field-name="timesUsed" + persist="width hidden" + hidden="true" + /> + <splitter class="tree-splitter" /> + </treecols> + <treechildren /> + </tree> + <separator class="thin" /> + <hbox id="SignonViewerButtons"> + <button + id="removeSignon" + disabled="true" + data-l10n-id="remove" + oncommand="DeleteSignon();" + /> + <button id="removeAllSignons" oncommand="DeleteAllSignons();" /> + <spacer flex="1" /> + <button id="togglePasswords" oncommand="TogglePasswordVisible();" /> + </hbox> + </vbox> + <hbox align="end"> + <hbox class="actionButtons" flex="1"> + <spacer flex="1" /> + <button + oncommand="window.close();" + data-l10n-id="password-close-button" + /> + </hbox> + </hbox> +</window> diff --git a/comm/mail/components/preferences/permissions.js b/comm/mail/components/preferences/permissions.js new file mode 100644 index 0000000000..3a75bc0ec6 --- /dev/null +++ b/comm/mail/components/preferences/permissions.js @@ -0,0 +1,501 @@ +/* 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/. */ + +// toolkit/content/treeUtils.js +/* globals gTreeUtils */ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +var NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions"; + +/** + * Magic URI base used so the permission manager can store + * remote content permissions for a given email address. + */ +var MAILURI_BASE = "chrome://messenger/content/email="; + +function Permission(principal, type, capability) { + this.principal = principal; + this.origin = principal.origin; + this.type = type; + this.capability = capability; +} + +var gPermissionManager = { + _type: "", + _permissions: [], + _permissionsToAdd: new Map(), + _permissionsToDelete: new Map(), + _tree: null, + _observerRemoved: false, + + _view: { + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), + _rowCount: 0, + get rowCount() { + return this._rowCount; + }, + getCellText(aRow, aColumn) { + if (aColumn.id == "siteCol") { + return gPermissionManager._permissions[aRow].origin.replace( + MAILURI_BASE, + "" + ); + } else if (aColumn.id == "statusCol") { + return gPermissionManager._permissions[aRow].capability; + } + return ""; + }, + + isSeparator(aIndex) { + return false; + }, + isSorted() { + return false; + }, + isContainer(aIndex) { + return false; + }, + setTree(aTree) {}, + getImageSrc(aRow, aColumn) {}, + getProgressMode(aRow, aColumn) {}, + getCellValue(aRow, aColumn) {}, + cycleHeader(column) {}, + getRowProperties(row) { + return ""; + }, + getColumnProperties(column) { + return ""; + }, + getCellProperties(row, column) { + if (column.element.getAttribute("id") == "siteCol") { + return "ltr"; + } + return ""; + }, + }, + + async _getCapabilityString(aCapability) { + var stringKey = null; + switch (aCapability) { + case Ci.nsIPermissionManager.ALLOW_ACTION: + stringKey = "permission-can-label"; + break; + case Ci.nsIPermissionManager.DENY_ACTION: + stringKey = "permission-cannot-label"; + break; + case Ci.nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY: + stringKey = "permission-can-access-first-party-label"; + break; + case Ci.nsICookiePermission.ACCESS_SESSION: + stringKey = "permission-can-session-label"; + break; + } + let string = await document.l10n.formatValue(stringKey); + return string; + }, + + async addPermission(aCapability) { + var textbox = document.getElementById("url"); + var input_url = textbox.value.trim(); + let principal; + try { + // The origin accessor on the principal object will throw if the + // principal doesn't have a canonical origin representation. This will + // help catch cases where the URI parser parsed something like + // `localhost:8080` as having the scheme `localhost`, rather than being + // an invalid URI. A canonical origin representation is required by the + // permission manager for storage, so this won't prevent any valid + // permissions from being entered by the user. + let uri; + try { + uri = Services.io.newURI(input_url); + principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + // If we have ended up with an unknown scheme, the following will throw. + principal.origin; + } catch (ex) { + let scheme = + this._type != "image" || !input_url.includes("@") + ? "http://" + : MAILURI_BASE; + uri = Services.io.newURI(scheme + input_url); + principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + // If we have ended up with an unknown scheme, the following will throw. + principal.origin; + } + } catch (ex) { + let [title, message] = await document.l10n.formatValues([ + { id: "invalid-uri-title" }, + { id: "invalid-uri-message" }, + ]); + Services.prompt.alert(window, title, message); + return; + } + + var capabilityString = await this._getCapabilityString(aCapability); + + // check whether the permission already exists, if not, add it + let permissionExists = false; + let capabilityExists = false; + for (var i = 0; i < this._permissions.length; ++i) { + // Thunderbird compares origins, not principals here. + if (this._permissions[i].principal.origin == principal.origin) { + permissionExists = true; + capabilityExists = this._permissions[i].capability == capabilityString; + if (!capabilityExists) { + this._permissions[i].capability = capabilityString; + } + break; + } + } + + let permissionParams = { + principal, + type: this._type, + capability: aCapability, + }; + if (!permissionExists) { + this._permissionsToAdd.set(principal.origin, permissionParams); + this._addPermission(permissionParams); + } else if (!capabilityExists) { + this._permissionsToAdd.set(principal.origin, permissionParams); + this._handleCapabilityChange(); + } + + textbox.value = ""; + textbox.focus(); + + // covers a case where the site exists already, so the buttons don't disable + this.onHostInput(textbox); + + // enable "remove all" button as needed + document.getElementById("removeAllPermissions").disabled = + this._permissions.length == 0; + }, + + _removePermission(aPermission) { + this._removePermissionFromList(aPermission.principal); + + // If this permission was added during this session, let's remove + // it from the pending adds list to prevent calls to the + // permission manager. + let isNewPermission = this._permissionsToAdd.delete( + aPermission.principal.origin + ); + + if (!isNewPermission) { + this._permissionsToDelete.set(aPermission.principal.origin, aPermission); + } + }, + + _handleCapabilityChange() { + // Re-do the sort, if the status changed from Block to Allow + // or vice versa, since if we're sorted on status, we may no + // longer be in order. + if (this._lastPermissionSortColumn == "statusCol") { + this._resortPermissions(); + } + this._tree.invalidate(); + }, + + _addPermission(aPermission) { + this._addPermissionToList(aPermission); + ++this._view._rowCount; + this._tree.rowCountChanged(this._view.rowCount - 1, 1); + // Re-do the sort, since we inserted this new item at the end. + this._resortPermissions(); + }, + + _resortPermissions() { + gTreeUtils.sort( + this._tree, + this._view, + this._permissions, + this._lastPermissionSortColumn, + this._permissionsComparator, + this._lastPermissionSortColumn, + !this._lastPermissionSortAscending + ); // keep sort direction + }, + + onHostInput(aSiteField) { + document.getElementById("btnSession").disabled = !aSiteField.value; + document.getElementById("btnBlock").disabled = !aSiteField.value; + document.getElementById("btnAllow").disabled = !aSiteField.value; + }, + + onWindowKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE) { + window.close(); + } + }, + + onHostKeyPress(aEvent) { + if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { + document.getElementById("btnAllow").click(); + } + }, + + onLoad() { + var params = window.arguments[0]; + this.init(params); + }, + + init(aParams) { + if (this._type) { + // reusing an open dialog, clear the old observer + this.uninit(); + } + + this._type = aParams.permissionType; + this._manageCapability = aParams.manageCapability; + + var permissionsText = document.getElementById("permissionsText"); + while (permissionsText.hasChildNodes()) { + permissionsText.lastChild.remove(); + } + permissionsText.appendChild(document.createTextNode(aParams.introText)); + + document.title = aParams.windowTitle; + + document.getElementById("btnBlock").hidden = !aParams.blockVisible; + document.getElementById("btnSession").hidden = !aParams.sessionVisible; + document.getElementById("btnAllow").hidden = !aParams.allowVisible; + + var urlFieldVisible = + aParams.blockVisible || aParams.sessionVisible || aParams.allowVisible; + + var urlField = document.getElementById("url"); + urlField.value = aParams.prefilledHost; + urlField.hidden = !urlFieldVisible; + + this.onHostInput(urlField); + + var urlLabel = document.getElementById("urlLabel"); + urlLabel.hidden = !urlFieldVisible; + + let treecols = document.getElementsByTagName("treecols")[0]; + treecols.addEventListener("click", event => { + if (event.target.nodeName != "treecol" || event.button != 0) { + return; + } + + let sortField = event.target.getAttribute("data-field-name"); + if (!sortField) { + return; + } + + gPermissionManager.onPermissionSort(sortField); + }); + + Services.obs.notifyObservers( + null, + NOTIFICATION_FLUSH_PERMISSIONS, + this._type + ); + Services.obs.addObserver(this, "perm-changed"); + + this._loadPermissions().then(() => urlField.focus()); + }, + + uninit() { + if (!this._observerRemoved) { + Services.obs.removeObserver(this, "perm-changed"); + + this._observerRemoved = true; + } + }, + + observe(aSubject, aTopic, aData) { + if (aTopic == "perm-changed") { + var permission = aSubject.QueryInterface(Ci.nsIPermission); + + // Ignore unrelated permission types. + if (permission.type != this._type) { + return; + } + + if (aData == "added") { + this._addPermission(permission); + } else if (aData == "changed") { + for (var i = 0; i < this._permissions.length; ++i) { + if (permission.matches(this._permissions[i].principal, true)) { + this._permissions[i].capability = this._getCapabilityString( + permission.capability + ); + break; + } + } + this._handleCapabilityChange(); + } else if (aData == "deleted") { + this._removePermissionFromList(permission); + } + } + }, + + onPermissionSelected() { + var hasSelection = this._tree.view.selection.count > 0; + var hasRows = this._tree.view.rowCount > 0; + document.getElementById("removePermission").disabled = + !hasRows || !hasSelection; + document.getElementById("removeAllPermissions").disabled = !hasRows; + }, + + onPermissionDeleted() { + if (!this._view.rowCount) { + return; + } + var removedPermissions = []; + gTreeUtils.deleteSelectedItems( + this._tree, + this._view, + this._permissions, + removedPermissions + ); + for (var i = 0; i < removedPermissions.length; ++i) { + var p = removedPermissions[i]; + this._removePermission(p); + } + document.getElementById("removePermission").disabled = + !this._permissions.length; + document.getElementById("removeAllPermissions").disabled = + !this._permissions.length; + }, + + onAllPermissionsDeleted() { + if (!this._view.rowCount) { + return; + } + var removedPermissions = []; + gTreeUtils.deleteAll( + this._tree, + this._view, + this._permissions, + removedPermissions + ); + for (var i = 0; i < removedPermissions.length; ++i) { + var p = removedPermissions[i]; + this._removePermission(p); + } + document.getElementById("removePermission").disabled = true; + document.getElementById("removeAllPermissions").disabled = true; + }, + + onPermissionKeyPress(aEvent) { + if ( + aEvent.keyCode == KeyEvent.DOM_VK_DELETE || + (AppConstants.platform == "macosx" && + aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE) + ) { + this.onPermissionDeleted(); + } + }, + + _lastPermissionSortColumn: "", + _lastPermissionSortAscending: false, + _permissionsComparator(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }, + + onPermissionSort(aColumn) { + this._lastPermissionSortAscending = gTreeUtils.sort( + this._tree, + this._view, + this._permissions, + aColumn, + this._permissionsComparator, + this._lastPermissionSortColumn, + this._lastPermissionSortAscending + ); + this._lastPermissionSortColumn = aColumn; + }, + + onApplyChanges() { + // Stop observing permission changes since we are about + // to write out the pending adds/deletes and don't need + // to update the UI + this.uninit(); + + for (let permissionParams of this._permissionsToAdd.values()) { + Services.perms.addFromPrincipal( + permissionParams.principal, + permissionParams.type, + permissionParams.capability + ); + } + + for (let p of this._permissionsToDelete.values()) { + Services.perms.removeFromPrincipal(p.principal, p.type); + } + + window.close(); + }, + + async _loadPermissions() { + this._tree = document.getElementById("permissionsTree"); + this._permissions = []; + + for (let perm of Services.perms.all) { + await this._addPermissionToList(perm); + } + + this._view._rowCount = this._permissions.length; + + // sort and display the table + this._tree.view = this._view; + this.onPermissionSort("origin"); + + // disable "remove all" button if there are none + document.getElementById("removeAllPermissions").disabled = + this._permissions.length == 0; + }, + + async _addPermissionToList(aPermission) { + if ( + aPermission.type == this._type && + (!this._manageCapability || + aPermission.capability == this._manageCapability) + ) { + var principal = aPermission.principal; + var capabilityString = await this._getCapabilityString( + aPermission.capability + ); + var p = new Permission(principal, aPermission.type, capabilityString); + this._permissions.push(p); + } + }, + + _removePermissionFromList(aPrincipal) { + for (let i = 0; i < this._permissions.length; ++i) { + // Thunderbird compares origins, not principals here. + if (this._permissions[i].principal.origin == aPrincipal.origin) { + this._permissions.splice(i, 1); + this._view._rowCount--; + this._tree.rowCountChanged(this._view.rowCount - 1, -1); + this._tree.invalidate(); + break; + } + } + }, + + setOrigin(aOrigin) { + document.getElementById("url").value = aOrigin; + }, +}; + +function setOrigin(aOrigin) { + gPermissionManager.setOrigin(aOrigin); +} + +function initWithParams(aParams) { + gPermissionManager.init(aParams); +} diff --git a/comm/mail/components/preferences/permissions.xhtml b/comm/mail/components/preferences/permissions.xhtml new file mode 100644 index 0000000000..33340d3f63 --- /dev/null +++ b/comm/mail/components/preferences/permissions.xhtml @@ -0,0 +1,128 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?> + +<!DOCTYPE dialog> + +<window + id="PermissionsDialog" + class="windowDialog" + data-l10n-id="permissions-reminder-window2" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="gPermissionManager.onLoad();" + onunload="gPermissionManager.uninit();" + persist="width height" + onkeypress="gPermissionManager.onWindowKeyPress(event);" +> + <script src="chrome://messenger/content/globalOverlay.js" /> + <script src="chrome://global/content/editMenuOverlay.js" /> + <script src="chrome://global/content/treeUtils.js" /> + <script src="chrome://messenger/content/preferences/permissions.js" /> + + <linkset> + <html:link + rel="localization" + href="messenger/preferences/permissions.ftl" + /> + </linkset> + + <keyset> + <key + data-l10n-id="permission-preferences-close-window" + data-l10n-attrs="key" + modifiers="accel" + oncommand="window.close();" + /> + </keyset> + + <vbox class="contentPane largeDialogContainer" flex="1"> + <description id="permissionsText" control="url" /> + <separator class="thin" /> + <label id="urlLabel" control="url" data-l10n-id="website-address-label" /> + <hbox align="start" class="input-container"> + <html:input + id="url" + type="text" + oninput="gPermissionManager.onHostInput(event.target);" + onkeypress="gPermissionManager.onHostKeyPress(event);" + /> + </hbox> + <hbox pack="end"> + <button + id="btnBlock" + disabled="true" + data-l10n-id="block-button" + oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.DENY_ACTION);" + /> + <button + id="btnSession" + disabled="true" + data-l10n-id="allow-session-button" + oncommand="gPermissionManager.addPermission(Ci.nsICookiePermission.ACCESS_SESSION);" + /> + <button + id="btnAllow" + disabled="true" + data-l10n-id="allow-button" + default="true" + oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);" + /> + </hbox> + <separator class="thin" /> + <tree + id="permissionsTree" + flex="1" + style="height: 18em" + hidecolumnpicker="true" + onkeypress="gPermissionManager.onPermissionKeyPress(event)" + onselect="gPermissionManager.onPermissionSelected();" + > + <treecols> + <treecol + id="siteCol" + data-l10n-id="treehead-sitename-label" + data-field-name="rawHost" + persist="width" + /> + <splitter class="tree-splitter" /> + <treecol + id="statusCol" + data-l10n-id="treehead-status-label" + data-field-name="capability" + persist="width" + /> + </treecols> + <treechildren /> + </tree> + </vbox> + <vbox> + <hbox class="actionButtons" flex="1"> + <button + id="removePermission" + disabled="true" + data-l10n-id="remove-site-button" + oncommand="gPermissionManager.onPermissionDeleted();" + /> + <button + id="removeAllPermissions" + data-l10n-id="remove-all-site-button" + oncommand="gPermissionManager.onAllPermissionsDeleted();" + /> + </hbox> + <spacer flex="1" /> + <hbox class="actionButtons" pack="end" flex="1"> + <button oncommand="window.close();" data-l10n-id="cancel-button" /> + <button + id="btnApplyChanges" + oncommand="gPermissionManager.onApplyChanges();" + data-l10n-id="save-button" + /> + </hbox> + </vbox> +</window> diff --git a/comm/mail/components/preferences/preferences.js b/comm/mail/components/preferences/preferences.js new file mode 100644 index 0000000000..1a123527ca --- /dev/null +++ b/comm/mail/components/preferences/preferences.js @@ -0,0 +1,453 @@ +/* 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 ../../../../toolkit/content/preferencesBindings.js */ +/* import-globals-from general.js */ +/* import-globals-from compose.js */ +/* import-globals-from downloads.js */ +/* import-globals-from privacy.js */ +/* import-globals-from chat.js */ +/* import-globals-from sync.js */ +/* import-globals-from findInPage.js */ +/* globals gCalendarPane */ + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { ExtensionSupport } = ChromeUtils.import( + "resource:///modules/ExtensionSupport.jsm" +); +var { calendarDeactivator } = ChromeUtils.import( + "resource:///modules/calendar/calCalendarDeactivator.jsm" +); +var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm"); +var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm"); + +var paneDeck = document.getElementById("paneDeck"); +var defaultPane = "paneGeneral"; + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(this, "gSubDialog", function () { + const { SubDialogManager } = ChromeUtils.importESModule( + "resource://gre/modules/SubDialog.sys.mjs" + ); + return new SubDialogManager({ + dialogStack: document.getElementById("dialogStack"), + dialogTemplate: document.getElementById("dialogTemplate"), + dialogOptions: { + styleSheets: [ + "chrome://messenger/skin/preferences/dialog.css", + "chrome://messenger/skin/preferences/preferences.css", + ], + resizeCallback: ({ title, frame }) => { + UIFontSize.registerWindow(frame.contentWindow); + + // Search within main document and highlight matched keyword. + gSearchResultsPane.searchWithinNode(title, gSearchResultsPane.query); + + // Search within sub-dialog document and highlight matched keyword. + gSearchResultsPane.searchWithinNode( + frame.contentDocument.firstElementChild, + gSearchResultsPane.query + ); + + // Creating tooltips for all the instances found + for (let node of gSearchResultsPane.listSearchTooltips) { + if (!node.tooltipNode) { + gSearchResultsPane.createSearchTooltip( + node, + gSearchResultsPane.query + ); + } + } + + // Resize the dialog to fit the content with edited font size. + requestAnimationFrame(() => { + let dialogs = frame.ownerGlobal.gSubDialog._dialogs; + let dialog = dialogs.find( + d => d._frame.contentDocument == frame.contentDocument + ); + if (dialog) { + UIFontSize.resizeSubDialog(dialog); + } + }); + }, + }, + }); +}); + +document.addEventListener("DOMContentLoaded", init, { once: true }); + +var gCategoryInits = new Map(); +var gLastCategory = { category: undefined, subcategory: undefined }; + +function init_category_if_required(category) { + let categoryInfo = gCategoryInits.get(category); + if (!categoryInfo) { + throw new Error( + "Unknown in-content prefs category! Can't init " + category + ); + } + if (categoryInfo.inited) { + return null; + } + return categoryInfo.init(); +} + +function register_module(categoryName, categoryObject) { + gCategoryInits.set(categoryName, { + inited: false, + async init() { + let template = document.getElementById(categoryName); + if (template) { + // Replace the template element with the nodes inside of it. + let frag = template.content; + await document.l10n.translateFragment(frag); + + // Actually insert them into the DOM. + document.l10n.pauseObserving(); + template.replaceWith(frag); + document.l10n.resumeObserving(); + + // Asks Preferences to update the attribute value of the entire + // document again (this can be simplified if we could separate the + // preferences of each pane.) + Preferences.queueUpdateOfAllElements(); + } + categoryObject.init(); + this.inited = true; + }, + }); +} + +function init() { + register_module("paneGeneral", gGeneralPane); + register_module("paneCompose", gComposePane); + register_module("panePrivacy", gPrivacyPane); + register_module("paneCalendar", gCalendarPane); + if (AppConstants.NIGHTLY_BUILD) { + register_module("paneSync", gSyncPane); + } + register_module("paneSearchResults", gSearchResultsPane); + if (Services.prefs.getBoolPref("mail.chat.enabled")) { + register_module("paneChat", gChatPane); + } else { + // Remove the pane from the DOM so it doesn't get incorrectly included in + // the search results. + document.getElementById("paneChat").remove(); + } + + // If no calendar is currently enabled remove it from the DOM so it doesn't + // get incorrectly included in the search results. + if (!calendarDeactivator.isCalendarActivated) { + document.getElementById("paneCalendar").remove(); + document.getElementById("category-calendar").remove(); + } + gSearchResultsPane.init(); + + let categories = document.getElementById("categories"); + categories.addEventListener("select", event => gotoPref(event.target.value)); + + document.documentElement.addEventListener("keydown", event => { + if (event.key == "Tab") { + categories.setAttribute("keyboard-navigation", "true"); + } else if ((event.ctrlKey || event.metaKey) && event.key == "f") { + document.getElementById("searchInput").focus(); + event.preventDefault(); + } + }); + + categories.addEventListener("mousedown", function () { + this.removeAttribute("keyboard-navigation"); + }); + + window.addEventListener("hashchange", onHashChange); + let lastSelected = Services.xulStore.getValue( + "about:preferences", + "paneDeck", + "lastSelected" + ); + gotoPref(lastSelected); + + UIDensity.registerWindow(window); + UIFontSize.registerWindow(window); +} + +function onHashChange() { + gotoPref(); +} + +async function gotoPref(aCategory) { + let categories = document.getElementById("categories"); + const kDefaultCategoryInternalName = "paneGeneral"; + const kDefaultCategory = "general"; + let hash = document.location.hash; + + let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName; + let breakIndex = category.indexOf("-"); + // Subcategories allow for selecting smaller sections of the preferences + // until proper search support is enabled (bug 1353954). + let subcategory = breakIndex != -1 && category.substring(breakIndex + 1); + if (subcategory) { + category = category.substring(0, breakIndex); + } + category = friendlyPrefCategoryNameToInternalName(category); + if (category != "paneSearchResults") { + gSearchResultsPane.query = null; + gSearchResultsPane.searchInput.value = ""; + gSearchResultsPane.getFindSelection(window).removeAllRanges(); + gSearchResultsPane.removeAllSearchTooltips(); + gSearchResultsPane.removeAllSearchMenuitemIndicators(); + } else if (!gSearchResultsPane.searchInput.value) { + // Something tried to send us to the search results pane without + // a query string. Default to the General pane instead. + category = kDefaultCategoryInternalName; + document.location.hash = kDefaultCategory; + gSearchResultsPane.query = null; + } + + // Updating the hash (below) or changing the selected category + // will re-enter gotoPref. + if (gLastCategory.category == category && !subcategory) { + return; + } + + let item; + if (category != "paneSearchResults") { + // Hide second level headers in normal view + for (let element of document.querySelectorAll(".search-header")) { + element.hidden = true; + } + + item = categories.querySelector(".category[value=" + category + "]"); + if (!item) { + category = kDefaultCategoryInternalName; + item = categories.querySelector(".category[value=" + category + "]"); + } + } + + if ( + gLastCategory.category || + category != kDefaultCategoryInternalName || + subcategory + ) { + let friendlyName = internalPrefCategoryNameToFriendlyName(category); + document.location.hash = friendlyName; + } + // Need to set the gLastCategory before setting categories.selectedItem since + // the categories 'select' event will re-enter the gotoPref codepath. + gLastCategory.category = category; + gLastCategory.subcategory = subcategory; + if (item) { + categories.selectedItem = item; + } else { + categories.clearSelection(); + } + window.history.replaceState(category, document.title); + + try { + await init_category_if_required(category); + } catch (ex) { + console.error( + new Error( + "Error initializing preference category " + category + ": " + ex + ) + ); + throw ex; + } + + // Bail out of this goToPref if the category + // or subcategory changed during async operation. + if ( + gLastCategory.category !== category || + gLastCategory.subcategory !== subcategory + ) { + return; + } + + search(category, "data-category"); + + let mainContent = document.querySelector(".main-content"); + mainContent.scrollTop = 0; + + spotlight(subcategory, category); + + document.dispatchEvent(new CustomEvent("paneSelected", { bubbles: true })); + document.getElementById("preferencesContainer").scrollTo(0, 0); + document.getElementById("paneDeck").setAttribute("lastSelected", category); + Services.xulStore.setValue( + "about:preferences", + "paneDeck", + "lastSelected", + category + ); +} + +function friendlyPrefCategoryNameToInternalName(aName) { + if (aName.startsWith("pane")) { + return aName; + } + return "pane" + aName.substring(0, 1).toUpperCase() + aName.substr(1); +} + +// This function is duplicated inside of utilityOverlay.js's openPreferences. +function internalPrefCategoryNameToFriendlyName(aName) { + return (aName || "").replace(/^pane./, function (toReplace) { + return toReplace[4].toLowerCase(); + }); +} + +function search(aQuery, aAttribute) { + let paneDeck = document.getElementById("paneDeck"); + let elements = paneDeck.children; + for (let element of elements) { + // If the "data-hidden-from-search" is "true", the + // element will not get considered during search. + if ( + element.getAttribute("data-hidden-from-search") != "true" || + element.getAttribute("data-subpanel") == "true" + ) { + let attributeValue = element.getAttribute(aAttribute); + if (attributeValue == aQuery) { + element.hidden = false; + } else { + element.hidden = true; + } + } else if ( + element.getAttribute("data-hidden-from-search") == "true" && + !element.hidden + ) { + element.hidden = true; + } + element.classList.remove("visually-hidden"); + } + + let keysets = paneDeck.getElementsByTagName("keyset"); + for (let element of keysets) { + let attributeValue = element.getAttribute(aAttribute); + if (attributeValue == aQuery) { + element.removeAttribute("disabled"); + } else { + element.setAttribute("disabled", true); + } + } +} + +async function spotlight(subcategory, category) { + let highlightedElements = document.querySelectorAll(".spotlight"); + if (highlightedElements.length) { + for (let element of highlightedElements) { + element.classList.remove("spotlight"); + } + } + if (subcategory) { + scrollAndHighlight(subcategory, category); + } +} + +async function scrollAndHighlight(subcategory, category) { + let element = document.querySelector(`[data-subcategory="${subcategory}"]`); + if (!element) { + return; + } + let header = getClosestDisplayedHeader(element); + + scrollContentTo(header); + element.classList.add("spotlight"); +} + +/** + * If there is no visible second level header it will return first level header, + * otherwise return second level header. + * + * @returns {Element} The closest displayed header. + */ +function getClosestDisplayedHeader(element) { + let header = element.closest("groupbox"); + let searchHeader = header.querySelector(".search-header"); + if ( + searchHeader && + searchHeader.hidden && + header.previousElementSibling.classList.contains("subcategory") + ) { + header = header.previousElementSibling; + } + return header; +} + +function scrollContentTo(element) { + const STICKY_CONTAINER_HEIGHT = + document.querySelector(".sticky-container").clientHeight; + let mainContent = document.querySelector(".main-content"); + let top = element.getBoundingClientRect().top - STICKY_CONTAINER_HEIGHT; + mainContent.scroll({ + top, + behavior: "smooth", + }); +} + +/** + * Selects the specified preferences pane + * + * @param paneID ID of prefpane to select + * @param scrollPaneTo ID of the element to scroll into view + * @param otherArgs.subdialog ID of button to activate, opening a subdialog + */ +function selectPrefPane(paneID, scrollPaneTo, otherArgs) { + if (paneID) { + if (gLastCategory.category != paneID) { + gotoPref(paneID); + } + if (scrollPaneTo) { + showTab(scrollPaneTo, otherArgs ? otherArgs.subdialog : undefined); + } + } +} + +/** + * Select the specified tab + * + * @param scrollPaneTo ID of the element to scroll into view + * @param subdialogID ID of button to activate, opening a subdialog + */ +function showTab(scrollPaneTo, subdialogID) { + setTimeout(function () { + let scrollTarget = document.getElementById(scrollPaneTo); + if (scrollTarget.closest("groupbox")) { + scrollTarget = scrollTarget.closest("groupbox"); + } + scrollTarget.scrollIntoView(); + if (subdialogID) { + document.getElementById(subdialogID).click(); + } + }); +} + +/** + * Filter the lastFallbackLocale from availableLocales if it doesn't have all + * of the needed strings. + * + * When the lastFallbackLocale isn't the defaultLocale, then by default only + * fluent strings are included. To fully use that locale you need the langpack + * to be installed, so if it isn't installed remove it from availableLocales. + */ +async function getAvailableLocales() { + let { availableLocales, defaultLocale, lastFallbackLocale } = Services.locale; + // If defaultLocale isn't lastFallbackLocale, then we still need the langpack + // for lastFallbackLocale for it to be useful. + if (defaultLocale != lastFallbackLocale) { + let lastFallbackId = `langpack-${lastFallbackLocale}@thunderbird.mozilla.org`; + let lastFallbackInstalled = await AddonManager.getAddonByID(lastFallbackId); + if (!lastFallbackInstalled) { + return availableLocales.filter(locale => locale != lastFallbackLocale); + } + } + return availableLocales; +} diff --git a/comm/mail/components/preferences/preferences.xhtml b/comm/mail/components/preferences/preferences.xhtml new file mode 100644 index 0000000000..8092f1af97 --- /dev/null +++ b/comm/mail/components/preferences/preferences.xhtml @@ -0,0 +1,256 @@ +<?xml version="1.0"?> +# 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/. + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://global/skin/popup.css"?> +<?xml-stylesheet href="chrome://global/skin/autocomplete.css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?> +<?xml-stylesheet href="chrome://messenger/skin/preferences/applications.css"?> +<?xml-stylesheet href="chrome://global/skin/in-content/common.css"?> +<?xml-stylesheet href="chrome://messenger/skin/preferences/preferences.css"?> +<?xml-stylesheet href="chrome://calendar/skin/shared/calendar-preferences.css" type="text/css"?> +<?xml-stylesheet href="chrome://calendar/skin/calendar.css"?> + +<!DOCTYPE html [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % editorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/editorOverlay.dtd"> +%editorOverlayDTD; +<!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> +%lightningDTD; +<!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd"> +%globalDTD; +<!ENTITY % eventDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd"> +%eventDTD; +]> + +<html id="MailPreferences" xmlns="http://www.w3.org/1999/xhtml" + role="document" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + scrolling="false"> +<head> + <title data-l10n-id="preferences-doc-title2"></title> + <meta http-equiv="Content-Security-Policy" content="default-src chrome:; connect-src *; script-src chrome: 'unsafe-inline'; img-src chrome: moz-icon: https: data:; style-src chrome: data: 'unsafe-inline'; object-src 'none'" /> + <meta name="color-scheme" content="light dark" /> + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="messenger/preferences/preferences.ftl" /> + <link rel="localization" href="messenger/preferences/fonts.ftl" /> + <link rel="localization" href="messenger/preferences/languages.ftl" /> + <link rel="localization" href="messenger/aboutDialog.ftl"/> + + <!-- Links below are only used for search-l10n-ids into subdialogs --> + <link rel="localization" href="messenger/preferences/receipts.ftl" /> + <link rel="localization" href="messenger/preferences/permissions.ftl" /> + <link rel="localization" href="messenger/preferences/cookies.ftl" /> + <link rel="localization" href="messenger/preferences/system-integration.ftl" /> + <link rel="localization" href="messenger/preferences/colors.ftl" /> + <link rel="localization" href="messenger/preferences/dock-options.ftl" /> + <link rel="localization" href="messenger/preferences/notifications.ftl" /> + <link rel="localization" href="messenger/preferences/new-tag.ftl" /> + <link rel="localization" href="toolkit/updates/history.ftl" /> + <link rel="localization" href="messenger/preferences/connection.ftl" /> + <link rel="localization" href="messenger/preferences/offline.ftl" /> + <link rel="localization" href="toolkit/about/config.ftl" /> + <link rel="localization" href="messenger/preferences/attachment-reminder.ftl" /> + <link rel="localization" href="messenger/preferences/passwordManager.ftl" /> + <link rel="localization" href="security/certificates/certManager.ftl" /> + <link rel="localization" href="security/certificates/deviceManager.ftl" /> +#ifdef NIGHTLY_BUILD + <link rel="localization" href="messenger/preferences/sync-dialog.ftl" /> + <link rel="localization" href="messenger/firefoxAccounts.ftl" /> +#endif + + <script defer="defer" src="chrome://global/content/preferencesBindings.js"></script> +#ifdef MOZ_UPDATER + <script defer="defer" src="chrome://messenger/content/aboutDialog-appUpdater.js"></script> +#endif + <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script> + <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script> + <script defer="defer" src="chrome://messenger/content/accountUtils.js"></script> + + <script defer="defer" src="chrome://communicator/content/contentAreaClick.js"></script> + <script defer="defer" src="chrome://messenger/content/preferences/preferences.js"></script> + <script defer="defer" src="chrome://messenger/content/preferences/findInPage.js"></script> + <script defer="defer" src="chrome://calendar/content/calendar-ui-utils.js"></script> + <script defer="defer" src="chrome://calendar/content/calendar-dialog-utils.js"></script> + <script defer="defer" src="chrome://calendar/content/preferences/general.js"></script> + <script defer="defer" src="chrome://calendar/content/preferences/alarms.js"></script> + <script defer="defer" src="chrome://calendar/content/widgets/calendar-notifications-setting.js"/> + <script defer="defer" src="chrome://calendar/content/preferences/notifications.js"/> + <script defer="defer" src="chrome://calendar/content/preferences/categories.js"></script> + <script defer="defer" src="chrome://calendar/content/preferences/views.js"></script> + <script defer="defer" src="chrome://calendar/content/preferences/calendar-preferences.js"></script> +</head> +<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <stack id="preferences-stack" flex="1"> + <hbox id="prefBox" class="main-content" flex="1"> + + <vbox id="pref-category-box"> + + <!-- category list --> + <richlistbox id="categories" + data-l10n-id="category-list" + data-l10n-attrs="aria-label"> + <richlistitem id="category-general" + class="category" + value="paneGeneral" + data-l10n-id="category-general" + align="center"> + <html:img class="category-icon" + src="chrome://messenger/skin/icons/new/touch/settings.svg" + alt="" /> + <label class="category-name" flex="1" data-l10n-id="pane-general-title"/> + </richlistitem> + + <richlistitem id="category-compose" + class="category" + value="paneCompose" + data-l10n-id="category-compose" + align="center"> + <html:img class="category-icon" + src="chrome://messenger/skin/icons/new/touch/pencil.svg" + alt="" /> + <label class="category-name" flex="1" data-l10n-id="pane-compose-title"/> + </richlistitem> + + <richlistitem id="category-privacy" + class="category" + value="panePrivacy" + data-l10n-id="category-privacy" + align="center"> + <html:img class="category-icon" + src="chrome://messenger/skin/icons/new/touch/lock.svg" + alt="" /> + <label class="category-name" flex="1" data-l10n-id="pane-privacy-title"/> + </richlistitem> + + <richlistitem id="category-chat" + class="category" + value="paneChat" + data-l10n-id="category-chat" + align="center"> + <html:img class="category-icon" + src="chrome://messenger/skin/icons/new/touch/chat.svg" + alt="" /> + <label class="category-name" flex="1" data-l10n-id="pane-chat-title"/> + </richlistitem> + + <richlistitem id="category-calendar" + class="category" + value="paneCalendar" + data-l10n-id="category-calendar" + align="center"> + <html:img class="category-icon" + src="chrome://messenger/skin/icons/new/touch/calendar.svg" + alt="" /> + <label class="category-name" flex="1" data-l10n-id="pane-calendar-title"/> + </richlistitem> +#ifdef NIGHTLY_BUILD + <richlistitem id="category-sync" + class="category" + value="paneSync" + data-l10n-id="category-sync" + align="center"> + <html:img class="category-icon" + src="chrome://messenger/skin/icons/new/touch/sync.svg" + alt="" /> + <label class="category-name" flex="1" data-l10n-id="pane-sync-title"/> + </richlistitem> +#endif + </richlistbox> + + <spacer flex="1"/> + + <vbox class="sidebar-footer-list"> + <html:a id="accountButton" class="sidebar-footer-link" + onclick="MsgAccountManager(null);"> + <html:img class="sidebar-footer-icon account-icon" + src="chrome://messenger/skin/icons/new/compact/account-settings.svg" + alt="" /> + <label data-l10n-id="account-button" class="sidebar-footer-label" flex="1"/> + </html:a> + + <html:a id="addonsButton" class="sidebar-footer-link" + onclick="window.browsingContext.topChromeWindow.openAddonsMgr();"> + <html:img class="sidebar-footer-icon" + src="chrome://messenger/skin/icons/new/compact/extension.svg" + alt="" /> + <label class="sidebar-footer-label" + data-l10n-id="open-addons-sidebar-button" + flex="1"/> + </html:a> + </vbox> + + </vbox> + + <vbox id="preferencesContainer" flex="1" align="start"> + <vbox class="paneDeckContainer"> + <hbox class="sticky-container" pack="end" align="top" flex="1"> + <search-textbox id="searchInput" + data-l10n-id="search-preferences-input2" + data-l10n-attrs="placeholder, style" + hidden="true"/> + </hbox> + <vbox id="paneDeck"> +#include searchResults.inc.xhtml +#include general.inc.xhtml +#include compose.inc.xhtml +#include privacy.inc.xhtml +#include chat.inc.xhtml +#include ../../../calendar/base/content/preferences/calendar-preferences.inc.xhtml +#ifdef NIGHTLY_BUILD +#include sync.inc.xhtml +#endif + </vbox> + </vbox> + </vbox> + </hbox> + <stack id="dialogStack" hidden="true"/> + <vbox id="dialogTemplate" + class="dialogOverlay" + align="center" + pack="center" + topmost="true" + hidden="true"> + <vbox class="dialogBox" + pack="end" + role="dialog" + aria-labelledby="dialogTitle"> + <hbox class="dialogTitleBar" align="center"> + <label class="dialogTitle" flex="1"/> + <button class="dialogClose close-icon" data-l10n-id="close-button"/> + </hbox> + <browser class="dialogFrame" + autoscroll="false" + disablehistory="true"/> + </vbox> + </vbox> + </stack> + + <!-- Helpers for the FileLink options browser select and autocomplete. --> + <popupset> + <!-- For select dropdowns. The menupopup is what shows the list of options, + and the popuponly menulist makes things like the menuactive attributes + work correctly on the menupopup. ContentSelectDropdown expects the + popuponly menulist to be its immediate parent. --> + <menulist popuponly="true" id="ContentSelectDropdown" hidden="true"> + <menupopup rolluponmousewheel="true" + activateontab="true" + position="after_start" + level="parent" +#ifdef XP_WIN + consumeoutsideclicks="false" + ignorekeys="shortcuts" +#endif + /> + </menulist> + <panel is="autocomplete-richlistbox-popup" id="PopupAutoComplete" + type="autocomplete" + role="group" + noautofocus="true"/> + </popupset> +</html:body> +</html> diff --git a/comm/mail/components/preferences/preferencesTab.js b/comm/mail/components/preferences/preferencesTab.js new file mode 100644 index 0000000000..0a59fda382 --- /dev/null +++ b/comm/mail/components/preferences/preferencesTab.js @@ -0,0 +1,162 @@ +/* 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/. */ + +// mail/base/content/specialTabs.js +/* globals contentTabBaseType, DOMLinkHandler */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { ExtensionParent } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionParent.sys.mjs" +); + +/** + * A tab to show Preferences. + */ +var preferencesTabType = { + __proto__: contentTabBaseType, + name: "preferencesTab", + perTabPanel: "vbox", + lastBrowserId: 0, + bundle: Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ), + protoSvc: Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService( + Ci.nsIExternalProtocolService + ), + + get loadingTabString() { + delete this.loadingTabString; + return (this.loadingTabString = document + .getElementById("bundle_messenger") + .getString("loadingTab")); + }, + + modes: { + preferencesTab: { + type: "preferencesTab", + }, + }, + + shouldSwitchTo(aArgs) { + if (!this.tab) { + return -1; + } + this.tab.browser.contentWindow.selectPrefPane( + aArgs.paneID, + aArgs.scrollPaneTo, + aArgs.otherArgs + ); + return document.getElementById("tabmail").tabInfo.indexOf(this.tab); + }, + + closeTab(aTab) { + this.tab = null; + }, + + openTab(aTab, aArgs) { + aTab.tabNode.setIcon( + "chrome://messenger/skin/icons/new/compact/settings.svg" + ); + + // First clone the page and set up the basics. + let clone = document + .getElementById("preferencesTab") + .firstElementChild.cloneNode(true); + + clone.setAttribute("id", "preferencesTab" + this.lastBrowserId); + clone.setAttribute("collapsed", false); + + aTab.panel.setAttribute("id", "preferencesTabWrapper" + this.lastBrowserId); + aTab.panel.appendChild(clone); + + // Start setting up the browser. + aTab.browser = aTab.panel.querySelector("browser"); + aTab.browser.setAttribute( + "id", + "preferencesTabBrowser" + this.lastBrowserId + ); + aTab.browser.setAttribute("autocompletepopup", "PopupAutoComplete"); + aTab.browser.addEventListener("DOMLinkAdded", DOMLinkHandler); + + aTab.findbar = document.createXULElement("findbar"); + aTab.findbar.setAttribute( + "browserid", + "preferencesTabBrowser" + this.lastBrowserId + ); + aTab.panel.appendChild(aTab.findbar); + + // Default to reload being disabled. + aTab.reloadEnabled = false; + + aTab.url = "about:preferences"; + aTab.paneID = aArgs.paneID; + aTab.scrollPaneTo = aArgs.scrollPaneTo; + aTab.otherArgs = aArgs.otherArgs; + + // Now set up the listeners. + this._setUpTitleListener(aTab); + this._setUpCloseWindowListener(aTab); + + // Wait for full loading of the tab and the automatic selecting of last tab. + // Then run the given onload code. + aTab.browser.addEventListener( + "paneSelected", + function (event) { + aTab.pageLoading = false; + aTab.pageLoaded = true; + + if ("onLoad" in aArgs) { + // Let selection of the initial pane complete before selecting another. + // Otherwise we can end up with two panes selected at once. + aTab.browser.contentWindow.setTimeout(() => { + // By now, the tab could already be closed. Check that it isn't. + if (aTab.panel) { + aArgs.onLoad(event, aTab.browser); + } + }, 0); + } + }, + { once: true } + ); + + // Initialize our unit testing variables. + aTab.pageLoading = true; + aTab.pageLoaded = false; + + // Now start loading the content. + aTab.title = this.loadingTabString; + + ExtensionParent.apiManager.emit("extension-browser-inserted", aTab.browser); + let params = { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + postData: aArgs.postData || null, + }; + aTab.browser.loadURI(Services.io.newURI("about:preferences"), params); + + this.tab = aTab; + this.lastBrowserId++; + }, + + persistTab(aTab) { + if (aTab.browser.currentURI.spec == "about:blank") { + return null; + } + + return { + paneID: aTab.paneID, + scrollPaneTo: aTab.scrollPaneTo, + otherArgs: aTab.otherArgs, + }; + }, + + restoreTab(aTabmail, aPersistedState) { + aTabmail.openTab("preferencesTab", { + paneID: aPersistedState.paneID, + scrollPaneTo: aPersistedState.scrollPaneTo, + otherArgs: aPersistedState.otherArgs, + }); + }, +}; diff --git a/comm/mail/components/preferences/privacy.inc.xhtml b/comm/mail/components/preferences/privacy.inc.xhtml new file mode 100644 index 0000000000..6afa5bb840 --- /dev/null +++ b/comm/mail/components/preferences/privacy.inc.xhtml @@ -0,0 +1,597 @@ +# 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/. + <script src="chrome://messenger/content/preferences/privacy.js"/> + + <stringbundle id="bundlePreferences" src="chrome://messenger/locale/preferences/preferences.properties"/> + <html:template id="panePrivacy"> + <hbox id="privacyCategory" + class="subcategory" + data-category="panePrivacy"> + <html:h1 data-l10n-id="privacy-main-header"></html:h1> + </hbox> + + <!-- Mail Content --> + <html:div data-category="panePrivacy"> + <html:fieldset id="mailContentGroup" data-category="panePrivacy"> + <html:legend data-l10n-id="mail-content"></html:legend> + <hbox id="remoteContentBox"> + <checkbox id="acceptRemoteContent" + preference="mailnews.message_display.disable_remote_image" + data-l10n-id="remote-content-label"/> + <spacer flex="1"/> + <hbox> + <button is="highlightable-button" id="remoteContentExceptions" + oncommand="gPrivacyPane.showRemoteContentExceptions();" + data-l10n-id="exceptions-button" + search-l10n-ids=" + permissions-reminder-window2.title, + website-address-label.value, + block-button.label, + allow-session-button.label, + allow-button.label, + treehead-sitename-label.label, + treehead-status-label.label, + remove-site-button.label, + remove-all-site-button.label, + cancel-button.label, + save-button.label, + permission-can-label, + permission-can-access-first-party-label, + permission-can-session-label, + permission-cannot-label, + invalid-uri-message, + invalid-uri-title"/> + </hbox> + </hbox> + <hbox> + <label is="text-link" id="acceptRemoteContentInfo" + href="https://support.mozilla.org/kb/remote-content-in-messages" + data-l10n-id="remote-content-info"/> + </hbox> + </html:fieldset> + </html:div> + + <!-- Web Content --> + <html:div data-category="panePrivacy"> + <html:fieldset id="webContentGroup" data-category="panePrivacy"> + <html:legend data-l10n-id="web-content"></html:legend> + <checkbox id="keepHistory" + preference="places.history.enabled" + data-l10n-id="history-label"/> + <hbox id="cookiesBox"> + <checkbox id="acceptCookies" + preference="network.cookie.cookieBehavior" + data-l10n-id="cookies-label"/> + <spacer flex="1"/> + <hbox> + <button is="highlightable-button" id="cookieExceptions" + oncommand="gPrivacyPane.showCookieExceptions();" + data-l10n-id="exceptions-button" + preference="pref.privacy.disable_button.cookie_exceptions" + search-l10n-ids=" + permissions-reminder-window2.title, + website-address-label.value, + block-button.label, + allow-session-button.label, + allow-button.label, + treehead-sitename-label.label, + treehead-status-label.label, + remove-site-button.label, + remove-all-site-button.label, + cancel-button.label, + save-button.label, + permission-can-label, + permission-can-access-first-party-label, + permission-can-session-label, + permission-cannot-label, + invalid-uri-message, + invalid-uri-title"/> + </hbox> + </hbox> + <hbox id="acceptThirdPartyRow" class="indent"> + <hbox id="acceptThirdPartyBox" align="center"> + <label id="acceptThirdPartyLabel" control="acceptThirdPartyMenu" + data-l10n-id="third-party-label"/> + <hbox> + <menulist id="acceptThirdPartyMenu" preference="network.cookie.cookieBehavior"> + <menupopup> + <menuitem data-l10n-id="third-party-always" value="always"/> + <menuitem data-l10n-id="third-party-visited" value="visited"/> + <menuitem data-l10n-id="third-party-never" value="never"/> + </menupopup> + </menulist> + </hbox> + </hbox> + <hbox flex="1"/> + <hbox> + <button is="highlightable-button" id="showCookiesButton" + data-l10n-id="cookies-button" + oncommand="gPrivacyPane.showCookies();" + preference="pref.privacy.disable_button.view_cookies" + search-l10n-ids=" + cookies-window-dialog2.title, + filter-search-label.value, + cookies-on-system-label, + treecol-site-header.label, + treecol-name-header.label, + props-name-label.value, + props-value-label.value, + props-domain-label.value, + props-path-label.value, + props-secure-label.value, + props-expires-label.value, + props-container-label.value, + remove-cookie-button.label, + remove-all-cookies-button.label, + cookie-close-button.label"/> + </hbox> + </hbox> + + <separator class="thin"/> + + <hbox align="center"> + <checkbox id="privacyDoNotTrackCheckbox" + class="tail-with-learn-more" + data-l10n-id="do-not-track-label" + preference="privacy.donottrackheader.enabled"/> + <label is="text-link" id="doNotTrackInfo" + href="https://www.mozilla.org/dnt" + data-l10n-id="dnt-learn-more-button"/> + </hbox> + </html:fieldset> + </html:div> + + <hbox id="privacyPasswordsCategory" + class="subcategory" + data-category="panePrivacy"> + <html:h1 data-l10n-id="privacy-passwords-header"></html:h1> + </hbox> + + <separator data-category="panePrivacy"/> + + <html:div data-category="panePrivacy"> + <html:fieldset data-category="panePrivacy"> + <hbox align="center"> + <description data-l10n-id="passwords-description"/> + <spacer flex="1"/> + <hbox> + <button is="highlightable-button" id="showPasswords" + data-l10n-id="passwords-button" + oncommand="gPrivacyPane.showPasswords();" + preference="pref.privacy.disable_button.view_passwords" + search-l10n-ids=" + saved-logins.title, + copy-provider-url-cmd.label, + copy-username-cmd.label, + edit-username-cmd.label, + copy-password-cmd.label, + edit-password-cmd.label, + search-filter.placeholder, + column-heading-provider.label, + column-heading-username.label, + column-heading-password.label, + column-heading-time-created.label, + column-heading-time-last-used.label, + column-heading-time-password-changed.label, + column-heading-times-used.label, + remove.label, + import.label, + show-passwords.label, + hide-passwords.label, + logins-description-all, + logins-description-filtered, + remove-all.label, + remove-all-shown.label, + remove-all-passwords-prompt, + remove-all-passwords-title, + no-master-password-prompt, + password-os-auth-dialog-message, + password-os-auth-dialog-message-macosx, + password-os-auth-dialog-caption"/> + </hbox> + </hbox> + <!-- XXX button to do a showExceptions()? --> + + <separator class="thin"/> + + <description data-l10n-id="primary-password-description"/> + <hbox> + <checkbox id="useMasterPassword" + data-l10n-id="primary-password-label" + oncommand="gPrivacyPane.updateMasterPasswordButton();"/> + <spacer flex="1"/> + <button is="highlightable-button" id="changeMasterPassword" + data-l10n-id="primary-password-button" + oncommand="gPrivacyPane.changeMasterPassword();"/> + </hbox> + <!-- + Those two strings are meant to be invisible and will be used exclusively to provide + localization for an alert window. + --> + <label id="fips-title" hidden="true" data-l10n-id="forms-primary-pw-fips-title"></label> + <label id="fips-desc" hidden="true" data-l10n-id="forms-master-pw-fips-desc"></label> + </html:fieldset> + </html:div> + + <hbox id="privacyJunkCategory" + class="subcategory" + data-category="panePrivacy"> + <html:h1 data-l10n-id="privacy-junk-header"></html:h1> + </hbox> + + <separator data-category="panePrivacy"/> + + <html:div data-category="panePrivacy"> + <html:fieldset data-category="panePrivacy"> + <description data-l10n-id="junk-description"/> + <separator class="thin"/> + <hbox> + <checkbox id="manualMark" + data-l10n-id="junk-label" + preference="mail.spam.manualMark" + oncommand="gPrivacyPane.updateManualMarkMode(this.checked);"/> + <spacer flex="1"/> + </hbox> + <radiogroup id="manualMarkMode" + class="indent" + preference="mail.spam.manualMarkMode" + aria-labelledby="manualMark"> + <hbox> + <radio id="manualMarkMode0" + value="0" + data-l10n-id="junk-move-label"/> + <spacer flex="1"/> + </hbox> + <hbox> + <radio id="manualMarkMode1" + value="1" + data-l10n-id="junk-delete-label"/> + <spacer flex="1"/> + </hbox> + </radiogroup> + <hbox> + <checkbox id="markAsReadOnSpam" + data-l10n-id="junk-read-label" + preference="mail.spam.markAsReadOnSpam"/> + <spacer flex="1"/> + </hbox> + <hbox align="start"> + <checkbox id="enableJunkLogging" data-l10n-id="junk-log-label" + oncommand="gPrivacyPane.updateJunkLogButton(this.checked);" + preference="mail.spam.logging.enabled"/> + <spacer flex="1"/> + <button is="highlightable-button" id="openJunkLogButton" + data-l10n-id="junk-log-button" + oncommand="gPrivacyPane.openJunkLog();"/> + </hbox> + <hbox align="start"> + <spacer flex="1"/> + <button is="highlightable-button" + data-l10n-id="reset-junk-button" + oncommand="gPrivacyPane.resetTrainingData()"/> + </hbox> + </html:fieldset> + </html:div> + +#ifdef MOZ_DATA_REPORTING + <hbox id="privacyDataCollectionCategory" + class="subcategory" + data-category="panePrivacy"> + <html:h1 data-l10n-id="collection-header"></html:h1> + </hbox> + + <html:div data-category="panePrivacy"> + <html:fieldset data-category="panePrivacy"> + <description> + <label class="tail-with-learn-more" data-l10n-id="collection-description"/> + <label id="dataCollectionPrivacyNotice" + class="learnMore" is="text-link" + data-l10n-id="collection-privacy-notice"/> + </description> + <description> + <html:div id="telemetry-container" hidden="hidden"> + <html:img id="telemetryInfoIcon" alt="" + src="chrome://global/skin/icons/info.svg" /> + <html:span id="telemetryDisabledDescription" + class="tail-with-learn-more" + data-l10n-id="collection-health-report-telemetry-disabled"> + </html:span> + <button id="telemetryDataDeletionLearnMore" + class="learnMore" is="text-link" + data-l10n-id="collection-health-report-telemetry-disabled-link"/> + </html:div> + </description> + <vbox data-subcategory="reports"> + <hbox align="center"> + <checkbox id="submitHealthReportBox" + data-l10n-id="collection-health-report" + class="tail-with-learn-more"/> + <label id="FHRLearnMore" + class="learnMore" is="text-link" + data-l10n-id="collection-health-report-link"/> + </hbox> +#ifndef MOZ_TELEMETRY_REPORTING + <description id="TelemetryDisabledDesc" + class="indent tip-caption" control="telemetryGroup" + data-l10n-id="collection-health-report-disabled"/> +#endif + +#ifdef MOZ_CRASHREPORTER + <hbox align="center"> + <checkbox id="automaticallySubmitCrashesBox" + class="tail-with-learn-more" + preference="browser.crashReports.unsubmittedCheck.autoSubmit2" + data-l10n-id="collection-backlogged-crash-reports"/> + <label id="crashReporterLearnMore" + class="learnMore" is="text-link" data-l10n-id="collection-backlogged-crash-reports-link"/> + </hbox> +#endif + </vbox> + </html:fieldset> + </html:div> +#endif + + <hbox id="privacySecurityCategory" + class="subcategory" + data-category="panePrivacy"> + <html:h1 data-l10n-id="privacy-security-header"></html:h1> + </hbox> + + <html:div data-category="panePrivacy"> + <html:fieldset data-category="panePrivacy"> + <html:legend data-l10n-id="privacy-scam-detection-title"></html:legend> + <description data-l10n-id="phishing-description"/> + <separator class="thin"/> + <hbox> + <checkbox id="enablePhishingDetector" + data-l10n-id="phishing-label" + preference="mail.phishing.detection.enabled"/> + <spacer flex="1"/> + </hbox> + </html:fieldset> + </html:div> + + <!-- Anti Virus --> + <html:div data-category="panePrivacy"> + <html:fieldset data-category="panePrivacy"> + <html:legend data-l10n-id="privacy-anti-virus-title"></html:legend> + <description data-l10n-id="antivirus-description"/> + <separator class="thin"/> + <hbox> + <checkbox id="enableAntiVirusQuarantine" + data-l10n-id="antivirus-label" + preference="mailnews.downloadToTempFile"/> + <spacer flex="1"/> + </hbox> + </html:fieldset> + </html:div> + + <html:div data-category="panePrivacy"> + <html:fieldset data-category="panePrivacy"> + <html:legend data-l10n-id="privacy-certificates-title"></html:legend> + <description id="CertSelectionDesc" control="certSelection" + data-l10n-id="certificate-description"/> + + <!-- + The values on these radio buttons may look like l12y issues, but + they're not - this preference uses *those strings* as its values. + I KID YOU NOT. + --> + <radiogroup id="certSelection" class="indent" + orient="horizontal" preftype="string" + preference="security.default_personal_cert" + aria-labelledby="CertGroupCaption CertSelectionDesc"> + <radio id="certSelectionAuto" + data-l10n-id="certificate-auto" + value="Select Automatically"/> + <radio id="certSelectionAsk" + data-l10n-id="certificate-ask" + value="Ask Every Time"/> + </radiogroup> + + <separator/> + + <hbox align="start"> + <checkbox id="enableOCSP" + data-l10n-id="ocsp-label" + preference="security.OCSP.enabled" + flex="1"/> + <spacer flex="1"/> + <vbox> + <hbox flex="1"> + <button is="highlightable-button" id="manageCertificatesButton" + data-l10n-id="certificate-button" + flex="1" + oncommand="gPrivacyPane.showCertificates();" + preference="security.disable_button.openCertManager" + search-l10n-ids=" + certmgr-title.title, + certmgr-tab-mine.label, + certmgr-tab-remembered.label, + certmgr-tab-people.label, + certmgr-tab-servers.label, + certmgr-tab-ca.label, + certmgr-mine, + certmgr-remembered, + certmgr-people, + certmgr-ca, + certmgr-server, + certmgr-edit-ca-cert2.title, + certmgr-edit-cert-edit-trust, + certmgr-edit-cert-trust-ssl.label, + certmgr-edit-cert-trust-email.label, + certmgr-delete-cert2.title, + certmgr-cert-host.label, + certmgr-cert-name.label, + certmgr-cert-server.label, + certmgr-token-name.label, + certmgr-begins-label.label, + certmgr-expires-label.label, + certmgr-email.label, + certmgr-serial.label, + certmgr-fingerprint-sha-256.label, + certmgr-view.label, + certmgr-edit.label, + certmgr-export.label, + certmgr-delete.label, + certmgr-delete-builtin.label, + certmgr-backup.label, + certmgr-backup-all.label, + certmgr-restore.label, + certmgr-add-exception.label, + exception-mgr.title, + exception-mgr-extra-button.label, + exception-mgr-supplemental-warning, + exception-mgr-cert-location-url.value, + exception-mgr-cert-location-download.label, + exception-mgr-cert-status-view-cert.label, + exception-mgr-permanent.label, + pk11-bad-password, + pkcs12-decode-err, + pkcs12-unknown-err-restore, + pkcs12-unknown-err-backup, + pkcs12-unknown-err, + pkcs12-info-no-smartcard-backup, + pkcs12-dup-data, + choose-p12-backup-file-dialog, + file-browse-pkcs12-spec, + choose-p12-restore-file-dialog, + file-browse-certificate-spec, + import-ca-certs-prompt, + import-email-cert-prompt, + delete-user-cert-title.title, + delete-user-cert-confirm, + delete-user-cert-impact, + delete-ca-cert-title.title, + delete-ca-cert-confirm, + delete-ca-cert-impact, + delete-ssl-override-title.title, + delete-ssl-override-confirm, + delete-ssl-override-impact, + delete-email-cert-title.title, + delete-email-cert-confirm, + delete-email-cert-impact, + send-no-client-certificate, + no-cert-stored-for-override, + permanent-override, + temporary-override, + add-exception-branded-warning, + add-exception-invalid-header, + add-exception-domain-mismatch-short, + add-exception-domain-mismatch-long, + add-exception-expired-short, + add-exception-expired-long, + add-exception-unverified-or-bad-signature-short, + add-exception-unverified-or-bad-signature-long, + add-exception-valid-short, + add-exception-valid-long, + add-exception-checking-short, + add-exception-checking-long, + add-exception-no-cert-short, + add-exception-no-cert-long, + save-cert-as, + cert-format-base64, + cert-format-base64-chain, + write-file-failure, + cert-format-der, + cert-format-pkcs7, + cert-format-pkcs7-chain"/> + </hbox> + <hbox flex="1"> + <button is="highlightable-button" id="viewSecurityDevicesButton" + data-l10n-id="security-devices-button" + flex="1" + oncommand="gPrivacyPane.showSecurityDevices();" + preference="security.disable_button.openDeviceManager" + search-l10n-ids=" + devmgr-window.title, + devmgr-devlist.label, + devmgr-header-details.label, + devmgr-header-value.label, + devmgr-button-login.label, + devmgr-button-logout.label, + devmgr-button-changepw.label, + devmgr-button-load.label, + devmgr-button-unload.label, + devmgr-button-enable-fips.label, + devmgr-button-disable-fips.label, + load-device.title, + load-device-info, + load-device-modname.value, + load-device-modname-default.value, + load-device-filename.value, + load-device-browse.label, + devinfo-status.label, + devinfo-status-disabled.label, + devinfo-status-not-present.label, + devinfo-status-uninitialized.label, + devinfo-status-not-logged-in.label, + devinfo-status-logged-in.label, + devinfo-status-ready.label, + devinfo-desc.label, + unable-to-toggle-fips, + load-pk11-module-file-picker-title, + fips-nonempty-primary-password-required, + load-module-help-root-certs-module-name.value, + add-module-failure, + del-module-warning, + del-module-error, + devinfo-man-id.label, + devinfo-hwversion.label, + load-module-help-empty-module-name.value, + devinfo-fwversion.label, + devinfo-modname.label, + devinfo-modpath.label, + login-failed, + devinfo-label.label, + devinfo-serialnum"/> + </hbox> + </vbox> + </hbox> + </html:fieldset> + </html:div> + + <!-- Email End-To-End Encryption --> + <hbox id="privacyCategory" + class="subcategory" + data-category="panePrivacy"> + <html:h1 data-l10n-id="email-e2ee-header"></html:h1> + </hbox> + + <html:div data-category="panePrivacy"> + <html:fieldset id="emailE2eeGroupPreparation" data-category="panePrivacy"> + <description data-l10n-id="email-e2ee-enable-info"/> + <html:button id="settingsButton" + type="button" + data-l10n-id="account-button" + class="button button-flat" + onclick="window.browsingContext.topChromeWindow.MsgAccountManager(null);"> + </html:button> + </html:fieldset> + </html:div> + + <html:div data-category="panePrivacy"> + <html:fieldset id="emailE2eeGroupAutomatism" data-category="panePrivacy"> + <html:legend data-l10n-id="email-e2ee-automatism"></html:legend> + <description data-l10n-id="email-e2ee-automatism-pre"/> + <separator class="thin"/> + + <checkbox id="emailE2eeAutoEnable" + preference="mail.e2ee.auto_enable" + data-l10n-id="email-e2ee-auto-on" + oncommand="gPrivacyPane.updateE2eeCheckboxes();"/> + <checkbox id="emailE2eeAutoDisable" + preference="mail.e2ee.auto_disable" + data-l10n-id="email-e2ee-auto-off" + oncommand="gPrivacyPane.updateE2eeCheckboxes();"/> + <checkbox id="emailE2eeAutoDisableNotify" + preference="mail.e2ee.notify_on_auto_disable" + data-l10n-id="email-e2ee-auto-off-notify" + oncommand="gPrivacyPane.updateE2eeCheckboxes();"/> + + <separator class="thin"/> + <description data-l10n-id="email-e2ee-automatism-post"/> + </html:fieldset> + </html:div> + </html:template> diff --git a/comm/mail/components/preferences/privacy.js b/comm/mail/components/preferences/privacy.js new file mode 100644 index 0000000000..2bb8cba6ad --- /dev/null +++ b/comm/mail/components/preferences/privacy.js @@ -0,0 +1,562 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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/. */ + +"use strict"; + +/* import-globals-from preferences.js */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", + OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs", +}); + +const PREF_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled"; + +Preferences.addAll([ + { id: "mail.spam.manualMark", type: "bool" }, + { id: "mail.spam.manualMarkMode", type: "int" }, + { id: "mail.spam.markAsReadOnSpam", type: "bool" }, + { id: "mail.spam.logging.enabled", type: "bool" }, + { id: "mail.phishing.detection.enabled", type: "bool" }, + { id: "browser.safebrowsing.enabled", type: "bool" }, + { id: "mailnews.downloadToTempFile", type: "bool" }, + { id: "pref.privacy.disable_button.view_passwords", type: "bool" }, + { id: "pref.privacy.disable_button.cookie_exceptions", type: "bool" }, + { id: "pref.privacy.disable_button.view_cookies", type: "bool" }, + { + id: "mailnews.message_display.disable_remote_image", + type: "bool", + inverted: "true", + }, + { id: "places.history.enabled", type: "bool" }, + { id: "network.cookie.cookieBehavior", type: "int" }, + { id: "network.cookie.blockFutureCookies", type: "bool" }, + { id: "privacy.donottrackheader.enabled", type: "bool" }, + { id: "security.default_personal_cert", type: "string" }, + { id: "security.disable_button.openCertManager", type: "bool" }, + { id: "security.disable_button.openDeviceManager", type: "bool" }, + { id: "security.OCSP.enabled", type: "int" }, + { id: "mail.e2ee.auto_enable", type: "bool" }, + { id: "mail.e2ee.auto_disable", type: "bool" }, + { id: "mail.e2ee.notify_on_auto_disable", type: "bool" }, +]); + +if (AppConstants.MOZ_DATA_REPORTING) { + Preferences.addAll([ + // Preference instances for prefs that we need to monitor while the page is open. + { id: PREF_UPLOAD_ENABLED, type: "bool" }, + ]); +} + +// Data Choices tab +if (AppConstants.MOZ_CRASHREPORTER) { + Preferences.add({ + id: "browser.crashReports.unsubmittedCheck.autoSubmit2", + type: "bool", + }); +} + +function setEventListener(aId, aEventType, aCallback) { + document + .getElementById(aId) + .addEventListener(aEventType, aCallback.bind(gPrivacyPane)); +} + +var gPrivacyPane = { + init() { + this.updateManualMarkMode(Preferences.get("mail.spam.manualMark").value); + this.updateJunkLogButton( + Preferences.get("mail.spam.logging.enabled").value + ); + + this._initMasterPasswordUI(); + + if (AppConstants.MOZ_DATA_REPORTING) { + this.initDataCollection(); + if (AppConstants.MOZ_CRASHREPORTER) { + this.initSubmitCrashes(); + } + this.initSubmitHealthReport(); + setEventListener( + "submitHealthReportBox", + "command", + gPrivacyPane.updateSubmitHealthReport + ); + setEventListener( + "telemetryDataDeletionLearnMore", + "command", + gPrivacyPane.showDataDeletion + ); + } + + this.readAcceptCookies(); + let element = document.getElementById("acceptCookies"); + Preferences.addSyncFromPrefListener(element, () => + this.readAcceptCookies() + ); + Preferences.addSyncToPrefListener(element, () => this.writeAcceptCookies()); + + element = document.getElementById("acceptThirdPartyMenu"); + Preferences.addSyncFromPrefListener(element, () => + this.readAcceptThirdPartyCookies() + ); + Preferences.addSyncToPrefListener(element, () => + this.writeAcceptThirdPartyCookies() + ); + + element = document.getElementById("enableOCSP"); + Preferences.addSyncFromPrefListener(element, () => this.readEnableOCSP()); + Preferences.addSyncToPrefListener(element, () => this.writeEnableOCSP()); + + this.initE2eeCheckboxes(); + }, + + /** + * Reload the current message after a preference affecting the view + * has been changed. + */ + reloadMessageInOpener() { + if (window.opener && typeof window.opener.ReloadMessage == "function") { + window.opener.ReloadMessage(); + } + }, + + /** + * Reads the network.cookie.cookieBehavior preference value and + * enables/disables the rest of the cookie UI accordingly, returning true + * if cookies are enabled. + */ + readAcceptCookies() { + let pref = Preferences.get("network.cookie.cookieBehavior"); + let exceptionsButton = document.getElementById("cookieExceptions"); + let acceptThirdPartyLabel = document.getElementById( + "acceptThirdPartyLabel" + ); + let acceptThirdPartyMenu = document.getElementById("acceptThirdPartyMenu"); + let showCookiesButton = document.getElementById("showCookiesButton"); + + // enable the rest of the UI for anything other than "disable all cookies" + let acceptCookies = pref.value != 2; + let cookieBehaviorLocked = Services.prefs.prefIsLocked( + "network.cookie.cookieBehavior" + ); + + exceptionsButton.disabled = cookieBehaviorLocked; + acceptThirdPartyLabel.disabled = acceptThirdPartyMenu.disabled = + !acceptCookies || cookieBehaviorLocked; + showCookiesButton.disabled = cookieBehaviorLocked; + + return acceptCookies; + }, + + /** + * Enables/disables the "keep until" label and menulist in response to the + * "accept cookies" checkbox being checked or unchecked. + * + * @returns 0 if cookies are accepted, 2 if they are not; + * the value network.cookie.cookieBehavior should get + */ + writeAcceptCookies() { + let accept = document.getElementById("acceptCookies"); + let acceptThirdPartyMenu = document.getElementById("acceptThirdPartyMenu"); + // if we're enabling cookies, automatically select 'accept third party always' + if (accept.checked) { + acceptThirdPartyMenu.selectedIndex = 0; + } + + return accept.checked ? 0 : 2; + }, + + /** + * Displays fine-grained, per-site preferences for cookies. + */ + showCookieExceptions() { + let bundle = document.getElementById("bundlePreferences"); + let params = { + blockVisible: true, + sessionVisible: true, + allowVisible: true, + prefilledHost: "", + permissionType: "cookie", + windowTitle: bundle.getString("cookiepermissionstitle"), + introText: bundle.getString("cookiepermissionstext"), + }; + gSubDialog.open( + "chrome://messenger/content/preferences/permissions.xhtml", + undefined, + params + ); + }, + + /** + * Displays all the user's cookies in a dialog. + */ + showCookies(aCategory) { + gSubDialog.open("chrome://messenger/content/preferences/cookies.xhtml"); + }, + + /** + * Converts between network.cookie.cookieBehavior and the third-party cookie UI + */ + readAcceptThirdPartyCookies() { + let pref = Preferences.get("network.cookie.cookieBehavior"); + switch (pref.value) { + case 0: + return "always"; + case 1: + return "never"; + case 2: + return "never"; + case 3: + return "visited"; + default: + return undefined; + } + }, + + writeAcceptThirdPartyCookies() { + let accept = document.getElementById("acceptThirdPartyMenu").selectedItem; + switch (accept.value) { + case "always": + return 0; + case "visited": + return 3; + case "never": + return 1; + default: + return undefined; + } + }, + + /** + * Displays fine-grained, per-site preferences for remote content. + * We use the "image" type for that, but it can also be stylesheets or + * iframes. + */ + showRemoteContentExceptions() { + let bundle = document.getElementById("bundlePreferences"); + let params = { + blockVisible: true, + sessionVisible: false, + allowVisible: true, + prefilledHost: "", + permissionType: "image", + windowTitle: bundle.getString("imagepermissionstitle"), + introText: bundle.getString("imagepermissionstext"), + }; + gSubDialog.open( + "chrome://messenger/content/preferences/permissions.xhtml", + undefined, + params + ); + }, + updateManualMarkMode(aEnableRadioGroup) { + document.getElementById("manualMarkMode").disabled = !aEnableRadioGroup; + }, + + updateJunkLogButton(aEnableButton) { + document.getElementById("openJunkLogButton").disabled = !aEnableButton; + }, + + openJunkLog() { + // The junk log dialog can't work as a sub-dialog, because that means + // loading it in a browser, and we can't load a chrome: page containing a + // file: page in a browser. Open it as a real dialog instead. + window.browsingContext.topChromeWindow.openDialog( + "chrome://messenger/content/junkLog.xhtml" + ); + }, + + resetTrainingData() { + // make sure the user really wants to do this + var bundle = document.getElementById("bundlePreferences"); + var title = bundle.getString("confirmResetJunkTrainingTitle"); + var text = bundle.getString("confirmResetJunkTrainingText"); + + // if the user says no, then just fall out + if (!Services.prompt.confirm(window, title, text)) { + return; + } + + // otherwise go ahead and remove the training data + MailServices.junk.resetTrainingData(); + }, + + /** + * Initializes primary password UI: the "use primary password" checkbox, selects + * the primary password button to show, and enables/disables it as necessary. + * The primary password is controlled by various bits of NSS functionality, + * so the UI for it can't be controlled by the normal preference bindings. + */ + _initMasterPasswordUI() { + var noMP = !LoginHelper.isPrimaryPasswordSet(); + + var button = document.getElementById("changeMasterPassword"); + button.disabled = noMP; + + var checkbox = document.getElementById("useMasterPassword"); + checkbox.checked = !noMP; + checkbox.disabled = + (noMP && !Services.policies.isAllowed("createMasterPassword")) || + (!noMP && !Services.policies.isAllowed("removeMasterPassword")); + }, + + /** + * Enables/disables the primary password button depending on the state of the + * "use primary password" checkbox, and prompts for primary password removal + * if one is set. + */ + async updateMasterPasswordButton() { + var checkbox = document.getElementById("useMasterPassword"); + var button = document.getElementById("changeMasterPassword"); + button.disabled = !checkbox.checked; + + // unchecking the checkbox should try to immediately remove the master + // password, because it's impossible to non-destructively remove the master + // password used to encrypt all the passwords without providing it (by + // design), and it would be extremely odd to pop up that dialog when the + // user closes the prefwindow and saves his settings + if (!checkbox.checked) { + await this._removeMasterPassword(); + } else { + await this.changeMasterPassword(); + } + + this._initMasterPasswordUI(); + }, + + /** + * Displays the "remove primary password" dialog to allow the user to remove + * the current primary password. When the dialog is dismissed, primary password + * UI is automatically updated. + */ + async _removeMasterPassword() { + var secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService( + Ci.nsIPKCS11ModuleDB + ); + if (secmodDB.isFIPSEnabled) { + let title = document.getElementById("fips-title").textContent; + let desc = document.getElementById("fips-desc").textContent; + Services.prompt.alert(window, title, desc); + this._initMasterPasswordUI(); + } else { + gSubDialog.open("chrome://mozapps/content/preferences/removemp.xhtml", { + closingCallback: this._initMasterPasswordUI.bind(this), + }); + } + this._initMasterPasswordUI(); + }, + + /** + * Displays a dialog in which the primary password may be changed. + */ + async changeMasterPassword() { + // OS reauthenticate functionality is not available on Linux yet (bug 1527745) + if ( + !LoginHelper.isPrimaryPasswordSet() && + Services.prefs.getBoolPref("signon.management.page.os-auth.enabled") && + AppConstants.platform != "linux" + ) { + let messageId = + "primary-password-os-auth-dialog-message-" + AppConstants.platform; + let [messageText, captionText] = await document.l10n.formatMessages([ + { + id: messageId, + }, + { + id: "master-password-os-auth-dialog-caption", + }, + ]); + let win = Services.wm.getMostRecentWindow(""); + let loggedIn = await OSKeyStore.ensureLoggedIn( + messageText.value, + captionText.value, + win, + false + ); + if (!loggedIn.authenticated) { + return; + } + } + + gSubDialog.open("chrome://mozapps/content/preferences/changemp.xhtml", { + closingCallback: this._initMasterPasswordUI.bind(this), + }); + }, + + /** + * Shows the sites where the user has saved passwords and the associated + * login information. + */ + showPasswords() { + gSubDialog.open( + "chrome://messenger/content/preferences/passwordManager.xhtml" + ); + }, + + updateDownloadedPhishingListState() { + document.getElementById("useDownloadedList").disabled = + !document.getElementById("enablePhishingDetector").checked; + }, + + /** + * Display the user's certificates and associated options. + */ + showCertificates() { + gSubDialog.open("chrome://pippki/content/certManager.xhtml"); + }, + + /** + * security.OCSP.enabled is an integer value for legacy reasons. + * A value of 1 means OCSP is enabled. Any other value means it is disabled. + */ + readEnableOCSP() { + var preference = Preferences.get("security.OCSP.enabled"); + // This is the case if the preference is the default value. + if (preference.value === undefined) { + return true; + } + return preference.value == 1; + }, + + /** + * See documentation for readEnableOCSP. + */ + writeEnableOCSP() { + var checkbox = document.getElementById("enableOCSP"); + return checkbox.checked ? 1 : 0; + }, + + /** + * Display a dialog from which the user can manage his security devices. + */ + showSecurityDevices() { + gSubDialog.open("chrome://pippki/content/device_manager.xhtml"); + }, + + /** + * Displays the learn more health report page when a user opts out of data collection. + */ + showDataDeletion() { + let url = + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "telemetry-clientid"; + window.open(url, "_blank"); + }, + + initDataCollection() { + this._setupLearnMoreLink( + "toolkit.datacollection.infoURL", + "dataCollectionPrivacyNotice" + ); + }, + + initSubmitCrashes() { + this._setupLearnMoreLink( + "toolkit.crashreporter.infoURL", + "crashReporterLearnMore" + ); + }, + + /** + * Set up or hide the Learn More links for various data collection options + */ + _setupLearnMoreLink(pref, element) { + // set up the Learn More link with the correct URL + let url = Services.urlFormatter.formatURLPref(pref); + let el = document.getElementById(element); + + if (url) { + el.setAttribute("href", url); + } else { + el.setAttribute("hidden", "true"); + } + }, + + /** + * Initialize the health report service reference and checkbox. + */ + initSubmitHealthReport() { + this._setupLearnMoreLink( + "datareporting.healthreport.infoURL", + "FHRLearnMore" + ); + + let checkbox = document.getElementById("submitHealthReportBox"); + + // Telemetry is only sending data if MOZ_TELEMETRY_REPORTING is defined. + // We still want to display the preferences panel if that's not the case, but + // we want it to be disabled and unchecked. + if ( + Services.prefs.prefIsLocked(PREF_UPLOAD_ENABLED) || + !AppConstants.MOZ_TELEMETRY_REPORTING + ) { + checkbox.setAttribute("disabled", "true"); + return; + } + + checkbox.checked = + Services.prefs.getBoolPref(PREF_UPLOAD_ENABLED) && + AppConstants.MOZ_TELEMETRY_REPORTING; + }, + + /** + * Update the health report preference with state from checkbox. + */ + updateSubmitHealthReport() { + let checkbox = document.getElementById("submitHealthReportBox"); + + Services.prefs.setBoolPref(PREF_UPLOAD_ENABLED, checkbox.checked); + + // If allow telemetry is checked, hide the box saying you're no longer + // allowing it. + document.getElementById("telemetry-container").hidden = checkbox.checked; + }, + + initE2eeCheckboxes() { + let on = document.getElementById("emailE2eeAutoEnable"); + let off = document.getElementById("emailE2eeAutoDisable"); + let notify = document.getElementById("emailE2eeAutoDisableNotify"); + + on.checked = Preferences.get("mail.e2ee.auto_enable").value; + off.checked = Preferences.get("mail.e2ee.auto_disable").value; + notify.checked = Preferences.get("mail.e2ee.notify_on_auto_disable").value; + + if (!on.checked) { + off.disabled = true; + notify.disabled = true; + } else { + off.disabled = false; + notify.disabled = !off.checked; + } + }, + + updateE2eeCheckboxes() { + let on = document.getElementById("emailE2eeAutoEnable"); + let off = document.getElementById("emailE2eeAutoDisable"); + let notify = document.getElementById("emailE2eeAutoDisableNotify"); + + if (!on.checked) { + off.disabled = true; + notify.disabled = true; + } else { + off.disabled = false; + notify.disabled = !off.checked; + } + }, +}; + +Preferences.get("mailnews.message_display.disable_remote_image").on( + "change", + gPrivacyPane.reloadMessageInOpener +); +Preferences.get("mail.phishing.detection.enabled").on( + "change", + gPrivacyPane.reloadMessageInOpener +); diff --git a/comm/mail/components/preferences/receipts.js b/comm/mail/components/preferences/receipts.js new file mode 100644 index 0000000000..748a65d127 --- /dev/null +++ b/comm/mail/components/preferences/receipts.js @@ -0,0 +1,38 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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 ../../../../toolkit/content/preferencesBindings.js */ + +Preferences.addAll([ + { id: "mail.receipt.request_return_receipt_on", type: "bool" }, + { id: "mail.incorporate.return_receipt", type: "int" }, + { id: "mail.mdn.report.enabled", type: "bool" }, + { id: "mail.mdn.report.not_in_to_cc", type: "int" }, + { id: "mail.mdn.report.outside_domain", type: "int" }, + { id: "mail.mdn.report.other", type: "int" }, +]); + +/** + * Enables/disables the labels and menulists depending whether + * sending of return receipts is enabled. + */ +function enableDisableAllowedReceipts() { + let enable = document.getElementById("receiptSend").value === "true"; + enableElement(document.getElementById("notInToCcLabel"), enable); + enableElement(document.getElementById("notInToCcPref"), enable); + enableElement(document.getElementById("outsideDomainLabel"), enable); + enableElement(document.getElementById("outsideDomainPref"), enable); + enableElement(document.getElementById("otherCasesLabel"), enable); + enableElement(document.getElementById("otherCasesPref"), enable); +} + +/** + * Set disabled state of aElement, unless its associated pref is locked. + */ +function enableElement(aElement, aEnable) { + let pref = aElement.getAttribute("preference"); + let prefIsLocked = pref ? Preferences.get(pref).locked : false; + aElement.disabled = !aEnable || prefIsLocked; +} diff --git a/comm/mail/components/preferences/receipts.xhtml b/comm/mail/components/preferences/receipts.xhtml new file mode 100644 index 0000000000..32f8a7237d --- /dev/null +++ b/comm/mail/components/preferences/receipts.xhtml @@ -0,0 +1,120 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> + +<!DOCTYPE window> + +<window + type="child" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="receipts-dialog-window" + onload="enableDisableAllowedReceipts();" +> + <dialog id="ReturnReceiptsDialog" dlgbuttons="accept,cancel"> + <script src="chrome://global/content/preferencesBindings.js" /> + <script src="chrome://messenger/content/preferences/receipts.js" /> + + <linkset> + <html:link rel="localization" href="messenger/preferences/receipts.ftl" /> + </linkset> + + <vbox id="returnReceiptSettings" align="start"> + <checkbox + id="alwaysRequest" + data-l10n-id="return-receipt-checkbox-control" + preference="mail.receipt.request_return_receipt_on" + /> + </vbox> + + <separator class="thin" /> + <separator class="groove" /> + <separator class="thin" /> + + <label control="receiptFolder" data-l10n-id="receipt-arrive-label" /> + <radiogroup + id="receiptFolder" + class="indent" + preference="mail.incorporate.return_receipt" + > + <radio value="0" data-l10n-id="receipt-leave-radio-control" /> + <radio value="1" data-l10n-id="receipt-move-radio-control" /> + </radiogroup> + + <separator class="thin" /> + <separator class="groove" /> + <separator class="thin" /> + + <label control="receiptSend" data-l10n-id="receipt-request-label" /> + <radiogroup + id="receiptSend" + class="indent" + preference="mail.mdn.report.enabled" + oncommand="enableDisableAllowedReceipts();" + > + <radio value="false" data-l10n-id="receipt-return-never-radio-control" /> + <radio value="true" data-l10n-id="receipt-return-some-radio-control" /> + + <vbox class="indent"> + <hbox align="center"> + <hbox flex="1"> + <label + id="notInToCcLabel" + data-l10n-id="receipt-not-to-cc-label" + control="notInToCcPref" + /> + </hbox> + <menulist + id="notInToCcPref" + preference="mail.mdn.report.not_in_to_cc" + > + <menupopup> + <menuitem value="0" data-l10n-id="receipt-send-never-label" /> + <menuitem value="1" data-l10n-id="receipt-send-always-label" /> + <menuitem value="2" data-l10n-id="receipt-send-ask-label" /> + </menupopup> + </menulist> + </hbox> + <hbox align="center"> + <hbox flex="1"> + <label + id="outsideDomainLabel" + data-l10n-id="sender-outside-domain-label" + control="outsideDomainPref" + /> + </hbox> + <menulist + id="outsideDomainPref" + preference="mail.mdn.report.outside_domain" + > + <menupopup> + <menuitem value="0" data-l10n-id="receipt-send-never-label" /> + <menuitem value="1" data-l10n-id="receipt-send-always-label" /> + <menuitem value="2" data-l10n-id="receipt-send-ask-label" /> + </menupopup> + </menulist> + </hbox> + <hbox align="center"> + <hbox flex="1"> + <label + id="otherCasesLabel" + data-l10n-id="other-cases-text-label" + control="otherCasesPref" + /> + </hbox> + <menulist id="otherCasesPref" preference="mail.mdn.report.other"> + <menupopup> + <menuitem value="0" data-l10n-id="receipt-send-never-label" /> + <menuitem value="1" data-l10n-id="receipt-send-always-label" /> + <menuitem value="2" data-l10n-id="receipt-send-ask-label" /> + </menupopup> + </menulist> + </hbox> + </vbox> + </radiogroup> + <separator /> + </dialog> +</window> diff --git a/comm/mail/components/preferences/searchResults.inc.xhtml b/comm/mail/components/preferences/searchResults.inc.xhtml new file mode 100644 index 0000000000..9fa66a27c5 --- /dev/null +++ b/comm/mail/components/preferences/searchResults.inc.xhtml @@ -0,0 +1,24 @@ +<!-- 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/. --> + + <hbox id="header-searchResults" + class="subcategory" + hidden="true" + data-hidden-from-search="true" + data-category="paneSearchResults"> + <html:h1 data-l10n-id="search-results-header"/> + </hbox> + <groupbox id="no-results-message" + data-hidden-from-search="true" + data-category="paneSearchResults" + hidden="true"> + <vbox class="no-results-container"> + <label id="sorry-message" data-l10n-id="search-results-empty-message2"> + <html:span data-l10n-name="query" id="sorry-message-query"/> + </label> + <label id="need-help" data-l10n-id="search-results-help-link"> + <html:a class="text-link" data-l10n-name="url" target="_blank"></html:a> + </label> + </vbox> + </groupbox> diff --git a/comm/mail/components/preferences/sync.inc.xhtml b/comm/mail/components/preferences/sync.inc.xhtml new file mode 100644 index 0000000000..a101d4359b --- /dev/null +++ b/comm/mail/components/preferences/sync.inc.xhtml @@ -0,0 +1,239 @@ +# 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/. +<script src="chrome://messenger/content/preferences/sync.js"/> + +<html:template id="paneSync"> + <html:div id="syncPaneCategory" + class="subcategory" + data-category="paneSync" + xmlns="http://www.w3.org/1999/xhtml"> + <h1 data-l10n-id="sync-pane-header"></h1> + + <section id="noFxaAccount" hidden="hidden"> + <div id="noFxaInfo"> + <h2 id="noFxaCaption" data-l10n-id="sync-signedout-caption"></h2> + <p id="noFxaDescription" + data-l10n-id="sync-signedout-description"> + </p> + <button id="noFxaSignIn" + class="primary" + type="button" + data-l10n-id="sync-signedout-account-signin-btn"> + </button> + </div> + <img id="noFxaSyncIllustration" + src="chrome://messenger/skin/illustrations/sync-devices.svg" + alt=""/> + </section> + + <section id="hasFxaAccount" hidden="hidden"> + <!-- Account NOT Verified --> + <section id="fxaLoginUnverified" + class="sync-account-section" + hidden="hidden"> + <div id="photoDisplay"> + <img class="contact-photo" src="" alt="" /> + </div> + <div id="fxaLoginUnverifiedInfo"> + <span id="fxaAccountMailNotVerified" + data-l10n-id="sync-pane-email-not-verified" + data-l10n-args='{"userEmail": ""}'> + </span> + <div id="fxaUnverifiedButtonOptions"> + <button id="fxaResendVerification" + class="place-self-end primary" + data-l10n-id="sync-pane-resend-verification" + type="button"> + </button> + <button id="fxaUnverifiedRemoveAccount" + class="place-self-end" + data-l10n-id="sync-pane-remove-account" + type="button"> + </button> + </div> + </div> + </section> + + <!-- Server Rejected Credientials --> + <section id="fxaLoginRejected" + class="sync-account-section" + hidden="hidden"> + <div id="photoDisplay"> + <img class="contact-photo" src="" alt="" /> + </div> + <div id="fxaLoginRejectedInfo"> + <span id="fxaAccountLoginRejected" + data-l10n-id="sync-signedin-login-failure" + data-l10n-args='{"userEmail": ""}'> + </span> + <div id="fxaRejectedButtonOptions"> + <button id="fxaRejectedSignIn" + class="place-self-end primary" + data-l10n-id="sync-pane-sign-in" + type="button"> + </button> + <button id="fxaRejectedRemoveAccount" + class="place-self-end" + data-l10n-id="sync-pane-remove-account" + type="button"> + </button> + </div> + </div> + </section> + + <!-- Account Verified --> + <section id="fxaLoginVerified" + class="sync-account-section" + hidden="hidden"> + <div id="photoInput"> + <button type="button" id="photoButton" + class="plain-button" + data-l10n-id="sync-pane-edit-photo"> + <img id="fxaAvatar" class="contact-photo" src="" alt="" /> + <div id="photoOverlay"></div> + </button> + </div> + <div id="fxaAccountInfo"> + <span id="fxaDisplayName"></span> + <span id="fxaEmailAddress"></span> + <a id="verifiedManage" href="#" data-l10n-id="sync-pane-manage-account"></a> + </div> + <button id="fxaAccountSignOut" + class="place-self-end" + data-l10n-id="sync-pane-sign-out" + type="button"> + </button> + </section> + + <fieldset id="fxaDeviceInfo" hidden="hidden"> + <legend data-l10n-id="sync-pane-device-name-title"></legend> + <input id="fxaDeviceNameInput" type="text" readonly="readonly"/> + <!-- Hidden by default, shown if #fxaDeviceNameChangeDeviceName is pressed --> + <button id="fxaDeviceNameCancel" + class="place-self-end" + data-l10n-id="sync-pane-cancel" + type="button" + hidden="hidden"> + </button> + <button id="fxaDeviceNameSave" + class="place-self-end" + data-l10n-id="sync-pane-save" + type="button" + hidden="hidden"> + </button> + <!-- Disappear once pressed to allow the previous two buttons to take + - its place, reappears once cancel or save is pressed --> + <button id="fxaDeviceNameChangeDeviceName" + class="place-self-end needs-account-ready" + data-l10n-id="sync-pane-change-device-name" + type="button"> + </button> + </fieldset> + + <div id="syncConnected" class="sync-section" hidden="hidden"> + <div id="showSyncedHeader"> + <h2 class="sync-header" + data-l10n-id="sync-pane-show-synced-header-on"> + </h2> + <button id="syncShowSyncedSyncNow" + class="place-self-end needs-account-ready" + data-l10n-id="sync-pane-sync-now" + type="button"> + </button> + </div> + + <div class="sync-panel"> + <div id="showSyncedListHeader"> + <h3 id="showSyncedListHeading" data-l10n-id="show-synced-list-heading"></h3> + <a id="enginesLearnMore" href="#" data-l10n-id="show-synced-learn-more"></a> + </div> + + <ul id="showSyncedList" class="synced-list"> + <!-- For when we get per-account controls: --> + <!-- <li id="showSyncAccount"> + <span id="showSyncAccountLabel" + class="synced-item" + data-l10n-id="show-synced-item-account"> + </span> + <div id="syncedAccounts"> + + <div class="synced-account"> + <h4 class="synced-account-name">nemo@thunderbird.net</h4> + <ul class="synced-list"> + <li class="synced-item synced-account-server-config" + data-l10n-id="synced-acount-item-server-config"> + </li> + <li class="synced-item synced-account-filters" + data-l10n-id="synced-acount-item-filters"> + </li> + <li class="synced-item synced-account-keys" + data-l10n-id="synced-acount-item-keys"> + </li> + </ul> + </div> + + <div class="synced-account"> + <h4 class="synced-account-name">example@example.com</h4> + <ul class="synced-list"> + <li class="synced-item synced-account-server-config" + data-l10n-id="synced-acount-item-server-config"> + </li> + <li class="synced-item synced-account-filters" + data-l10n-id="synced-acount-item-filters"> + </li> + <li class="synced-item synced-account-keys" + data-l10n-id="synced-acount-item-keys"> + </li> + </ul> + </div> + + </div> + </li> --> + <li id="showSyncAccount" + class="synced-item" + data-l10n-id="show-synced-item-account"> + </li> + <li id="showSyncIdentity" + class="synced-item" + data-l10n-id="show-synced-item-identity"> + </li> + <li id="showSyncAddress" + class="synced-item" + data-l10n-id="show-synced-item-address"> + </li> + <li id="showSyncCalendar" + class="synced-item" + data-l10n-id="show-synced-item-calendar"> + </li> + <li id="showSyncPasswords" + class="synced-item" + data-l10n-id="show-synced-item-passwords"> + </li> + </ul> + + <button id="syncChangeOptions" + class="place-self-end primary" + data-l10n-id="show-synced-change" + type="button"> + </button> + </div> + </div> + + <div id="syncDisconnected" class="sync-section" hidden="hidden"> + <h2 class="sync-header" + data-l10n-id="sync-pane-show-synced-header-off"> + </h2> + + <div class="sync-panel"> + <p data-l10n-id="sync-disconnected-text"></p> + <button id="syncSetup" + class="place-self-start needs-account-ready" + data-l10n-id="sync-disconnected-turn-on-sync" + type="button"> + </button> + </div> + </div> + </section> + </html:div> +</html:template> diff --git a/comm/mail/components/preferences/sync.js b/comm/mail/components/preferences/sync.js new file mode 100644 index 0000000000..d8336c6e23 --- /dev/null +++ b/comm/mail/components/preferences/sync.js @@ -0,0 +1,377 @@ +/* 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 preferences.js */ + +ChromeUtils.defineESModuleGetters(this, { + UIState: "resource://services-sync/UIState.sys.mjs", + Weave: "resource://services-sync/main.sys.mjs", +}); + +var { FxAccounts, getFxAccountsSingleton } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +var fxAccounts = getFxAccountsSingleton(); + +var gSyncPane = { + init() { + this._setupEventListeners(); + this.setupEnginesUI(); + + Weave.Svc.Obs.add(UIState.ON_UPDATE, this.updateWeavePrefs, this); + + window.addEventListener("unload", () => { + Weave.Svc.Obs.remove(UIState.ON_UPDATE, this.updateWeavePrefs, this); + }); + + let cachedComputerName = Services.prefs.getStringPref( + "identity.fxaccounts.account.device.name", + "" + ); + if (cachedComputerName) { + this._populateComputerName(cachedComputerName); + } + + this.updateWeavePrefs(); + }, + + /** + * Update the UI based on the current state. + */ + updateWeavePrefs() { + let state = UIState.get(); + + let noFxaAccount = document.getElementById("noFxaAccount"); + let hasFxaAccount = document.getElementById("hasFxaAccount"); + if (state.status == UIState.STATUS_NOT_CONFIGURED) { + noFxaAccount.hidden = false; + hasFxaAccount.hidden = true; + return; + } + noFxaAccount.hidden = true; + hasFxaAccount.hidden = false; + + let syncReady = false; // Is sync able to actually sync? + let fxaLoginUnverified = document.getElementById("fxaLoginUnverified"); + let fxaLoginRejected = document.getElementById("fxaLoginRejected"); + let fxaLoginVerified = document.getElementById("fxaLoginVerified"); + if (state.status == UIState.STATUS_LOGIN_FAILED) { + fxaLoginUnverified.hidden = true; + fxaLoginRejected.hidden = false; + fxaLoginVerified.hidden = true; + } else if (state.status == UIState.STATUS_NOT_VERIFIED) { + fxaLoginUnverified.hidden = false; + fxaLoginRejected.hidden = true; + fxaLoginVerified.hidden = true; + } else { + fxaLoginUnverified.hidden = true; + fxaLoginRejected.hidden = true; + fxaLoginVerified.hidden = false; + syncReady = true; + } + + this._populateComputerName(Weave.Service.clientsEngine.localName); + for (let elt of document.querySelectorAll(".needs-account-ready")) { + elt.disabled = !syncReady; + } + + let syncConnected = document.getElementById("syncConnected"); + let syncDisconnected = document.getElementById("syncDisconnected"); + syncConnected.hidden = !syncReady || !state.syncEnabled; + syncDisconnected.hidden = !syncReady || state.syncEnabled; + + document.l10n.setAttributes( + document.getElementById("fxaAccountMailNotVerified"), + "sync-pane-email-not-verified", + { userEmail: state.email } + ); + document.l10n.setAttributes( + document.getElementById("fxaAccountLoginRejected"), + "sync-signedin-login-failure", + { userEmail: state.email } + ); + + document.getElementById("fxaAvatar").src = + state.avatarURL && !state.avatarIsDefault ? state.avatarURL : ""; + document.getElementById("fxaDisplayName").textContent = state.displayName; + document.getElementById("fxaEmailAddress").textContent = state.email; + + this._updateSyncNow(state.syncing); + }, + + _toggleComputerNameControls(editMode) { + let textbox = document.getElementById("fxaDeviceNameInput"); + textbox.readOnly = !editMode; + document.getElementById("fxaDeviceNameChangeDeviceName").hidden = editMode; + document.getElementById("fxaDeviceNameCancel").hidden = !editMode; + document.getElementById("fxaDeviceNameSave").hidden = !editMode; + }, + + _focusComputerNameTextbox() { + let textbox = document.getElementById("fxaDeviceNameInput"); + let valLength = textbox.value.length; + textbox.focus(); + textbox.setSelectionRange(valLength, valLength); + }, + + _blurComputerNameTextbox() { + document.getElementById("fxaDeviceNameInput").blur(); + }, + + _focusAfterComputerNameTextbox() { + // Focus the most appropriate element that's *not* the "computer name" box. + Services.focus.moveFocus( + window, + document.getElementById("fxaDeviceNameInput"), + Services.focus.MOVEFOCUS_FORWARD, + 0 + ); + }, + + _updateComputerNameValue(save) { + if (save) { + let textbox = document.getElementById("fxaDeviceNameInput"); + Weave.Service.clientsEngine.localName = textbox.value; + } + this._populateComputerName(Weave.Service.clientsEngine.localName); + }, + + _setupEventListeners() { + function setEventListener(id, eventType, callback) { + document + .getElementById(id) + .addEventListener(eventType, callback.bind(gSyncPane)); + } + + setEventListener("noFxaSignIn", "click", function () { + window.browsingContext.topChromeWindow.gSync.initFxA(); + return false; + }); + setEventListener( + "fxaResendVerification", + "click", + gSyncPane.verifyFirefoxAccount + ); + setEventListener("fxaUnverifiedRemoveAccount", "click", function () { + /* No warning as account can't have previously synced. */ + gSyncPane.unlinkFirefoxAccount(false); + }); + setEventListener("fxaRejectedSignIn", "click", gSyncPane.reSignIn); + setEventListener("fxaRejectedRemoveAccount", "click", function () { + gSyncPane.unlinkFirefoxAccount(true); + }); + setEventListener("photoButton", "click", function (event) { + window.browsingContext.topChromeWindow.gSync.openFxAAvatarPage( + "preferences" + ); + }); + setEventListener("verifiedManage", "click", function (event) { + window.browsingContext.topChromeWindow.gSync.openFxAManagePage( + "preferences" + ); + event.preventDefault(); + // Stop attempts to open this link in an external browser. + event.stopPropagation(); + }); + setEventListener("fxaAccountSignOut", "click", function () { + gSyncPane.unlinkFirefoxAccount(true); + }); + setEventListener("fxaDeviceNameCancel", "click", function () { + // We explicitly blur the textbox because of bug 75324, then after + // changing the state of the buttons, force focus to whatever the focus + // manager thinks should be next (which on the mac, depends on an OSX + // keyboard access preference) + this._blurComputerNameTextbox(); + this._toggleComputerNameControls(false); + this._updateComputerNameValue(false); + this._focusAfterComputerNameTextbox(); + }); + setEventListener("fxaDeviceNameSave", "click", function () { + // Work around bug 75324 - see above. + this._blurComputerNameTextbox(); + this._toggleComputerNameControls(false); + this._updateComputerNameValue(true); + this._focusAfterComputerNameTextbox(); + }); + setEventListener("fxaDeviceNameChangeDeviceName", "click", function () { + this._toggleComputerNameControls(true); + this._focusComputerNameTextbox(); + }); + setEventListener("syncShowSyncedSyncNow", "click", function () { + // syncing can take a little time to send the "started" notification, so + // pretend we already got it. + this._updateSyncNow(true); + Weave.Service.sync({ why: "aboutprefs" }); + }); + setEventListener("enginesLearnMore", "click", function (event) { + // TODO: A real page. + window.browsingContext.topChromeWindow.openContentTab( + "https://example.org/?page=learnMore" + ); + event.preventDefault(); + // Stop attempts to open this link in an external browser. + event.stopPropagation(); + }); + setEventListener("syncChangeOptions", "click", function () { + gSyncPane._chooseWhatToSync(true); + }); + setEventListener("syncSetup", "click", function () { + gSyncPane._chooseWhatToSync(false); + }); + }, + + async _chooseWhatToSync(isAlreadySyncing) { + // Assuming another device is syncing and we're not, we update the engines + // selection so the correct checkboxes are pre-filled. + if (!isAlreadySyncing) { + try { + await Weave.Service.updateLocalEnginesState(); + } catch (err) { + console.error("Error updating the local engines state", err); + } + } + let params = {}; + if (isAlreadySyncing) { + // If we are already syncing then we also offer to disconnect. + params.disconnectFun = () => this.disconnectSync(); + } + gSubDialog.open( + "chrome://messenger/content/preferences/syncDialog.xhtml", + { + features: "resizable=no", + closingCallback: event => { + if (!isAlreadySyncing && event.detail.button == "accept") { + // We weren't syncing but the user has accepted the dialog - so we + // want to start! + fxAccounts.telemetry + .recordConnection(["sync"], "ui") + .then(() => { + return Weave.Service.configure(); + }) + .catch(err => { + console.error("Failed to enable sync", err); + }); + } + }, + }, + params + ); + }, + + _updateSyncNow(syncing) { + let button = document.getElementById("syncShowSyncedSyncNow"); + if (syncing) { + document.l10n.setAttributes(button, "sync-panel-sync-now-syncing"); + button.disabled = true; + } else { + document.l10n.setAttributes(button, "sync-pane-sync-now"); + button.disabled = false; + } + }, + + /** + * If connecting to Firefox Accounts failed, try again. + */ + async reSignIn() { + // There's a bit of an edge-case here - we might be forcing reauth when we've + // lost the FxA account data - in which case we'll not get a URL as the re-auth + // URL embeds account info and the server endpoint complains if we don't + // supply it - so we just use the regular "sign in" URL in that case. + if (!(await FxAccounts.canConnectAccount())) { + return; + } + + const url = + (await FxAccounts.config.promiseForceSigninURI("preferences")) || + (await FxAccounts.config.promiseConnectAccountURI("preferences")); + window.browsingContext.topChromeWindow.openContentTab(url); + }, + + /** + * Send a confirmation email to the account's email address. + */ + verifyFirefoxAccount() { + let onError = async () => { + let [title, body] = await document.l10n.formatValues([ + "fxa-verification-not-sent-title", + "fxa-verification-not-sent-body", + ]); + new Notification(title, { body }); + }; + + let onSuccess = async data => { + if (data) { + let [title, body] = await document.l10n.formatValues([ + "fxa-verification-sent-title", + { id: "fxa-verification-sent-body", args: { userEmail: data.email } }, + ]); + new Notification(title, { body }); + } else { + onError(); + } + }; + + fxAccounts + .resendVerificationEmail() + .then(() => fxAccounts.getSignedInUser(), onError) + .then(onSuccess, onError); + }, + + /** + * Disconnect the account, including everything linked. + * + * @param {boolean} confirm - If true, asks the user if they're sure. + */ + unlinkFirefoxAccount(confirm) { + window.browsingContext.topChromeWindow.gSync.disconnect({ confirm }); + }, + + /** + * Disconnect sync, leaving the FxA account connected. + */ + disconnectSync() { + return window.browsingContext.topChromeWindow.gSync.disconnect({ + confirm: true, + disconnectAccount: false, + }); + }, + + _populateComputerName(value) { + let textbox = document.getElementById("fxaDeviceNameInput"); + if (!textbox.hasAttribute("placeholder")) { + textbox.setAttribute( + "placeholder", + fxAccounts.device.getDefaultLocalName() + ); + } + textbox.value = value; + }, + + /** + * Arranges to dynamically show or hide sync engine name elements based on + * the preferences used for the engines. + */ + setupEnginesUI() { + let observe = (element, prefName) => { + element.hidden = !Services.prefs.getBoolPref(prefName, false); + }; + + let engineItems = { + showSyncAccount: "services.sync.engine.accounts", + showSyncIdentity: "services.sync.engine.identities", + showSyncAddress: "services.sync.engine.addressbooks", + showSyncCalendar: "services.sync.engine.calendars", + showSyncPasswords: "services.sync.engine.passwords", + }; + + for (let [id, prefName] of Object.entries(engineItems)) { + let obs = observe.bind(null, document.getElementById(id), prefName); + obs(); + Services.prefs.addObserver(prefName, obs); + window.addEventListener("unload", () => { + Services.prefs.removeObserver(prefName, obs); + }); + } + }, +}; diff --git a/comm/mail/components/preferences/syncDialog.js b/comm/mail/components/preferences/syncDialog.js new file mode 100644 index 0000000000..6920a8aa59 --- /dev/null +++ b/comm/mail/components/preferences/syncDialog.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const engineItems = { + configSyncAccount: "services.sync.engine.accounts", + configSyncAddress: "services.sync.engine.addressbooks", + configSyncCalendar: "services.sync.engine.calendars", + configSyncIdentity: "services.sync.engine.identities", + configSyncPasswords: "services.sync.engine.passwords", +}; + +window.addEventListener("load", function () { + for (let [id, prefName] of Object.entries(engineItems)) { + let element = document.getElementById(id); + element.checked = Services.prefs.getBoolPref(prefName, false); + } + + let options = window.arguments[0]; + if (options.disconnectFun) { + window.addEventListener("dialogextra2", function () { + options.disconnectFun().then(disconnected => { + if (disconnected) { + window.close(); + } + }); + }); + } else { + document.querySelector("dialog").getButton("extra2").hidden = true; + } +}); + +window.addEventListener("dialogaccept", function () { + for (let [id, prefName] of Object.entries(engineItems)) { + let element = document.getElementById(id); + Services.prefs.setBoolPref(prefName, element.checked); + } +}); diff --git a/comm/mail/components/preferences/syncDialog.xhtml b/comm/mail/components/preferences/syncDialog.xhtml new file mode 100644 index 0000000000..ce035245ea --- /dev/null +++ b/comm/mail/components/preferences/syncDialog.xhtml @@ -0,0 +1,210 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<!DOCTYPE html> +<html + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" +> + <head> + <title data-l10n-id="config-sync-dailog-title"></title> + + <link rel="stylesheet" href="chrome://global/skin/global.css" /> + <link + rel="stylesheet" + href="chrome://messenger/skin/preferences/preferences.css" + /> + + <link rel="localization" href="messenger/preferences/preferences.ftl" /> + <link rel="localization" href="messenger/preferences/sync-dialog.ftl" /> + + <script src="chrome://messenger/content/preferences/syncDialog.js"></script> + </head> + <body> + <xul:dialog + id="configSyncDialog" + buttons="accept,cancel,extra2" + style="min-width: 49em" + data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept, buttonlabelextra2, buttonaccesskeyextra2" + data-l10n-id="sync-dialog" + > + <div id="configSyncDialogContent"> + <ul id="configSyncList" class="config-list"> + <li id="configAccountsContainer"> + <div class="config-item"> + <input + type="checkbox" + id="configSyncAccount" + name="configSynced" + /> + <label + for="configSyncAccount" + id="configSyncAccountLabel" + data-l10n-id="show-synced-item-account" + > + </label> + </div> + <!-- For when we get per-account controls: --> + <!-- <div id="configAccounts"> + + <div class="synced-account"> + <div class="config-item synced-account-name"> + <input type="checkbox" + id="configSyncAccount_1" + name="configSyncedAccounts"/> + <label for="configSyncAccount_1"> + nemo@thunderbird.net + </label> + </div> + <ul class="config-list"> + <li> + <div class="config-item"> + <input type="checkbox" + id="configSyncServer_1" + name="configSyncedAccount_1"/> + <label for="configSyncServer_1" + class="synced-account-server-config" + data-l10n-id="synced-acount-item-server-config"> + </label> + </div> + </li> + <li> + <div class="config-item"> + <input type="checkbox" + id="configSyncFilters_1" + name="configSyncedAccount_1"/> + <label for="configSyncFilters_1" + class="synced-account-filters" + data-l10n-id="synced-acount-item-filters"> + </label> + </div> + </li> + <li> + <div class="config-item"> + <input type="checkbox" + id="configSyncKeys_1" + name="configSyncedAccount_1"/> + <label for="configSyncKeys_1" + class="synced-account-keys" + data-l10n-id="synced-acount-item-keys"> + </label> + </div> + </li> + </ul> + </div> + + <div class="synced-account"> + <div class="config-item synced-account-name"> + <input type="checkbox" + id="configSyncAccount_2" + name="configSyncedAccounts"/> + <label for="configSyncAccount_2"> + example@example.com + </label> + </div> + <ul class="config-list"> + <li> + <div class="config-item"> + <input type="checkbox" + id="configSyncServer_2" + name="configSyncedAccount_2"/> + <label for="configSyncServer_2" + class="synced-account-server-config" + data-l10n-id="synced-acount-item-server-config"> + </label> + </div> + </li> + <li> + <div class="config-item"> + <input type="checkbox" + id="configSyncFilters_2" + name="configSyncedAccount_2"/> + <label for="configSyncFilters_2" + class="synced-account-filters" + data-l10n-id="synced-acount-item-filters"> + </label> + </div> + </li> + <li> + <div class="config-item"> + <input type="checkbox" + id="configSyncKeys_2" + name="configSyncedAccount_2"/> + <label for="configSyncKeys_2" + class="synced-account-keys" + data-l10n-id="synced-acount-item-keys"> + </label> + </div> + </li> + </ul> + </div> + + </div> --> + </li> + <li> + <div class="config-item"> + <input + type="checkbox" + id="configSyncIdentity" + name="configSynced" + /> + <label + id="configSyncIdentityLabel" + for="configSyncIdentity" + data-l10n-id="show-synced-item-identity" + > + </label> + </div> + </li> + <li> + <div class="config-item"> + <input + type="checkbox" + id="configSyncAddress" + name="configSynced" + /> + <label + id="configSyncAddressLabel" + for="configSyncAddress" + data-l10n-id="show-synced-item-address" + > + </label> + </div> + </li> + <li> + <div class="config-item"> + <input + type="checkbox" + id="configSyncCalendar" + name="configSynced" + /> + <label + id="configSyncCalendarLabel" + for="configSyncCalendar" + data-l10n-id="show-synced-item-calendar" + > + </label> + </div> + </li> + <li> + <div class="config-item"> + <input + type="checkbox" + id="configSyncPasswords" + name="configSynced" + /> + <label + id="configSyncPasswordsLabel" + for="configSyncPasswords" + data-l10n-id="show-synced-item-passwords" + > + </label> + </div> + </li> + </ul> + </div> + </xul:dialog> + </body> +</html> diff --git a/comm/mail/components/preferences/tagDialog.xhtml b/comm/mail/components/preferences/tagDialog.xhtml new file mode 100644 index 0000000000..32dd72268d --- /dev/null +++ b/comm/mail/components/preferences/tagDialog.xhtml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> + +<!DOCTYPE window> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + data-l10n-id="tag-dialog-window" + style="min-width: 25em;" + onload="onLoad();"> +<dialog> + + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + + <script src="chrome://messenger/content/globalOverlay.js"/> + <script src="chrome://global/content/editMenuOverlay.js"/> + <script src="chrome://messenger/content/newTagDialog.js"/> +#include ../../base/content/tagDialog.inc.xhtml +</dialog> +</window> diff --git a/comm/mail/components/preferences/test/browser/browser.ini b/comm/mail/components/preferences/test/browser/browser.ini new file mode 100644 index 0000000000..44b7d97a31 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/browser.ini @@ -0,0 +1,20 @@ +[DEFAULT] +head = head.js +prefs = + mail.provider.suppress_dialog_on_startup=true + mail.spotlight.firstRunDone=true + mail.winsearch.firstRunDone=true + mailnews.start_page.override_url=about:blank + mailnews.start_page.url=about:blank +subsuite = thunderbird + +[browser_chat.js] +[browser_cloudfile.js] +support-files = files/icon.svg files/management.html +[browser_compose.js] +[browser_general.js] +[browser_openPreferences.js] +[browser_privacy.js] +[browser_sync.js] +skip-if = !nightly_build +support-files = files/avatar.png diff --git a/comm/mail/components/preferences/test/browser/browser_chat.js b/comm/mail/components/preferences/test/browser/browser_chat.js new file mode 100644 index 0000000000..009f2a9211 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/browser_chat.js @@ -0,0 +1,74 @@ +/* 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/. */ + +add_task(async () => { + await testCheckboxes( + "paneChat", + "chatPaneCategory", + { + checkboxID: "reportIdle", + pref: "messenger.status.reportIdle", + enabledElements: ["#autoAway", "#timeBeforeAway"], + }, + { + checkboxID: "sendTyping", + pref: "purple.conversations.im.send_typing", + }, + { + checkboxID: "desktopChatNotifications", + pref: "mail.chat.show_desktop_notifications", + }, + { + checkboxID: "getAttention", + pref: "messenger.options.getAttentionOnNewMessages", + }, + { + checkboxID: "chatNotification", + pref: "mail.chat.play_sound", + enabledElements: ["#chatSoundType radio"], + } + ); + + Services.prefs.setBoolPref("messenger.status.reportIdle", true); + await testCheckboxes("paneChat", "chatPaneCategory", { + checkboxID: "autoAway", + pref: "messenger.status.awayWhenIdle", + enabledElements: ["#defaultIdleAwayMessage"], + }); + + Services.prefs.setBoolPref("mail.chat.play_sound", true); + await testRadioButtons("paneChat", "chatPaneCategory", { + pref: "mail.chat.play_sound.type", + states: [ + { + id: "chatSoundSystemSound", + prefValue: 0, + }, + { + id: "chatSoundCustom", + prefValue: 1, + enabledElements: ["#chatSoundUrlLocation", "#browseForChatSound"], + }, + ], + }); +}); + +add_task(async function testMessageStylePreview() { + await openNewPrefsTab("paneChat", "chatPaneCategory"); + const conversationLoad = TestUtils.topicObserved("conversation-loaded"); + const [subject] = await conversationLoad; + do { + await BrowserTestUtils.waitForEvent(subject, "MessagesDisplayed"); + } while (subject.getPendingMessagesCount() > 0); + const messageParent = subject.contentChatNode; + let message = messageParent.firstElementChild; + const messages = new Set(); + while (message) { + ok(message._originalMsg); + messages.add(message._originalMsg); + message = message.nextElementSibling; + } + is(messages.size, 3, "All 3 messages displayed"); + await closePrefsTab(); +}); diff --git a/comm/mail/components/preferences/test/browser/browser_cloudfile.js b/comm/mail/components/preferences/test/browser/browser_cloudfile.js new file mode 100644 index 0000000000..9f395f9ab8 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/browser_cloudfile.js @@ -0,0 +1,796 @@ +/* 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/. */ + +/* eslint-env webextensions */ + +let { cloudFileAccounts } = ChromeUtils.import( + "resource:///modules/cloudFileAccounts.jsm" +); +let { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +function ManagementScript() { + browser.test.onMessage.addListener((message, assertMessage, browserStyle) => { + if (message !== "check-style") { + return; + } + function verifyButton(buttonElement, expected) { + let buttonStyle = window.getComputedStyle(buttonElement); + let buttonBackgroundColor = buttonStyle.backgroundColor; + if (browserStyle && expected.hasBrowserStyleClass) { + browser.test.assertEq( + "rgb(9, 150, 248)", + buttonBackgroundColor, + assertMessage + ); + } else { + browser.test.assertTrue( + buttonBackgroundColor !== "rgb(9, 150, 248)", + assertMessage + ); + } + } + + function verifyCheckboxOrRadio(element, expected) { + let style = window.getComputedStyle(element); + let styledBackground = element.checked + ? "rgb(9, 150, 248)" + : "rgb(255, 255, 255)"; + if (browserStyle && expected.hasBrowserStyleClass) { + browser.test.assertEq( + styledBackground, + style.backgroundColor, + assertMessage + ); + } else { + browser.test.assertTrue( + style.backgroundColor != styledBackground, + assertMessage + ); + } + } + + let normalButton = document.getElementById("normalButton"); + let browserStyleButton = document.getElementById("browserStyleButton"); + verifyButton(normalButton, { hasBrowserStyleClass: false }); + verifyButton(browserStyleButton, { hasBrowserStyleClass: true }); + + let normalCheckbox1 = document.getElementById("normalCheckbox1"); + let normalCheckbox2 = document.getElementById("normalCheckbox2"); + let browserStyleCheckbox = document.getElementById("browserStyleCheckbox"); + verifyCheckboxOrRadio(normalCheckbox1, { hasBrowserStyleClass: false }); + verifyCheckboxOrRadio(normalCheckbox2, { hasBrowserStyleClass: false }); + verifyCheckboxOrRadio(browserStyleCheckbox, { + hasBrowserStyleClass: true, + }); + + let normalRadio1 = document.getElementById("normalRadio1"); + let normalRadio2 = document.getElementById("normalRadio2"); + let browserStyleRadio = document.getElementById("browserStyleRadio"); + verifyCheckboxOrRadio(normalRadio1, { hasBrowserStyleClass: false }); + verifyCheckboxOrRadio(normalRadio2, { hasBrowserStyleClass: false }); + verifyCheckboxOrRadio(browserStyleRadio, { hasBrowserStyleClass: true }); + + browser.test.notifyPass("management-ui-browser_style"); + }); + browser.test.sendMessage("management-ui-ready"); +} + +let extension; +async function startExtension(browser_style) { + let cloud_file = { + name: "Mochitest", + management_url: "management.html", + }; + + switch (browser_style) { + case "true": + cloud_file.browser_style = true; + break; + case "false": + cloud_file.browser_style = false; + break; + } + + extension = ExtensionTestUtils.loadExtension({ + async background() { + browser.test.onMessage.addListener(async message => { + if (message != "set-configured") { + return; + } + let accounts = await browser.cloudFile.getAllAccounts(); + for (let account of accounts) { + await browser.cloudFile.updateAccount(account.id, { + configured: true, + }); + } + browser.test.sendMessage("ready"); + }); + }, + files: { + "management.html": `<html> + <body> + <a id="a" href="https://www.example.com/">Click me!</a> + <button id="normalButton" name="button" class="default">Default</button> + <button id="browserStyleButton" name="button" class="browser-style default">Default</button> + + <input id="normalCheckbox1" type="checkbox"/> + <input id="normalCheckbox2" type="checkbox"/><label>Checkbox</label> + <div class="browser-style"> + <input id="browserStyleCheckbox" type="checkbox"><label for="browserStyleCheckbox">Checkbox</label> + </div> + + <input id="normalRadio1" type="radio"/> + <input id="normalRadio2" type="radio"/><label>Radio</label> + <div class="browser-style"> + <input id="browserStyleRadio" checked="" type="radio"><label for="browserStyleRadio">Radio</label> + </div> + </body> + <script src="management.js" type="text/javascript"></script> + </html>`, + "management.js": ManagementScript, + }, + manifest: { + cloud_file, + applications: { gecko: { id: "cloudfile@mochitest" } }, + }, + }); + + info("Starting extension"); + await extension.startup(); + + if (accountIsConfigured) { + extension.sendMessage("set-configured"); + await extension.awaitMessage("ready"); + } +} + +add_task(async () => { + // Register a fake provider representing a built-in provider. We don't + // currently ship any built-in providers, but if we did, we should check + // if they are present before doing this. Built-in providers can be + // problematic for artifact builds. + cloudFileAccounts.registerProvider("Fake-Test", { + displayName: "XYZ Fake", + type: "ext-fake@extensions.thunderbird.net", + }); + registerCleanupFunction(() => { + cloudFileAccounts.unregisterProvider("Fake-Test"); + }); +}); + +let accountIsConfigured = false; + +// Mock the prompt service. We're going to be asked if we're sure +// we want to remove an account, so let's say yes. + +/** @implements {nsIPromptService} */ +let mockPromptService = { + confirmCount: 0, + confirm() { + this.confirmCount++; + return true; + }, + QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]), +}; +/** @implements {nsIExternalProtocolService} */ +let mockExternalProtocolService = { + _loadedURLs: [], + externalProtocolHandlerExists(aProtocolScheme) {}, + getApplicationDescription(aScheme) {}, + getProtocolHandlerInfo(aProtocolScheme) {}, + getProtocolHandlerInfoFromOS(aProtocolScheme, aFound) {}, + isExposedProtocol(aProtocolScheme) {}, + loadURI(aURI, aWindowContext) { + this._loadedURLs.push(aURI.spec); + }, + setProtocolHandlerDefaults(aHandlerInfo, aOSHandlerExists) {}, + urlLoaded(aURL) { + return this._loadedURLs.includes(aURL); + }, + QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]), +}; + +let originalPromptService = Services.prompt; +Services.prompt = mockPromptService; + +let mockExternalProtocolServiceCID = MockRegistrar.register( + "@mozilla.org/uriloader/external-protocol-service;1", + mockExternalProtocolService +); + +registerCleanupFunction(() => { + Services.prompt = originalPromptService; + MockRegistrar.unregister(mockExternalProtocolServiceCID); +}); + +add_task(async function addRemoveAccounts() { + is(cloudFileAccounts.providers.length, 1); + is(cloudFileAccounts.accounts.length, 0); + + // Load the preferences tab. + + let { prefsDocument, prefsWindow } = await openNewPrefsTab( + "paneCompose", + "compositionAttachmentsCategory" + ); + + // Check everything is as it should be. + + let accountList = prefsDocument.getElementById("cloudFileView"); + is(accountList.itemCount, 0); + + let buttonList = prefsDocument.getElementById("addCloudFileAccountButtons"); + ok(!buttonList.hidden); + is(buttonList.childElementCount, 1); + is( + buttonList.children[0].getAttribute("value"), + "ext-fake@extensions.thunderbird.net" + ); + + let menuButton = prefsDocument.getElementById("addCloudFileAccount"); + ok(menuButton.hidden); + is(menuButton.itemCount, 1); + is( + menuButton.getItemAtIndex(0).getAttribute("value"), + "ext-fake@extensions.thunderbird.net" + ); + + let removeButton = prefsDocument.getElementById("removeCloudFileAccount"); + ok(removeButton.disabled); + + let cloudFileDefaultPanel = prefsDocument.getElementById( + "cloudFileDefaultPanel" + ); + ok(!cloudFileDefaultPanel.hidden); + + let browserWrapper = prefsDocument.getElementById("cloudFileSettingsWrapper"); + is(browserWrapper.childElementCount, 0); + + // Register our test provider. + + await startExtension(); + is(cloudFileAccounts.providers.length, 2); + is(cloudFileAccounts.accounts.length, 0); + + await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve)); + + is(buttonList.childElementCount, 2); + is( + buttonList.children[0].getAttribute("value"), + "ext-fake@extensions.thunderbird.net" + ); + is(buttonList.children[1].getAttribute("value"), "ext-cloudfile@mochitest"); + is( + buttonList.children[1].style.listStyleImage, + `url("chrome://messenger/content/extension.svg")` + ); + + is(menuButton.itemCount, 2); + is( + menuButton.getItemAtIndex(0).getAttribute("value"), + "ext-fake@extensions.thunderbird.net" + ); + is( + menuButton.getItemAtIndex(1).getAttribute("value"), + "ext-cloudfile@mochitest" + ); + is( + menuButton.getItemAtIndex(1).getAttribute("image"), + "chrome://messenger/content/extension.svg" + ); + + // Create a new account. + + EventUtils.synthesizeMouseAtCenter( + buttonList.children[1], + { clickCount: 1 }, + prefsWindow + ); + is(cloudFileAccounts.accounts.length, 1); + is(cloudFileAccounts.configuredAccounts.length, 0); + + let account = cloudFileAccounts.accounts[0]; + let accountKey = account.accountKey; + is(cloudFileAccounts.accounts[0].type, "ext-cloudfile@mochitest"); + + // Check prefs were updated. + + is( + Services.prefs.getCharPref( + `mail.cloud_files.accounts.${accountKey}.displayName` + ), + "Mochitest" + ); + is( + Services.prefs.getCharPref(`mail.cloud_files.accounts.${accountKey}.type`), + "ext-cloudfile@mochitest" + ); + + // Check UI was updated. + + is(accountList.itemCount, 1); + is(accountList.selectedIndex, 0); + ok(!removeButton.disabled); + + let accountListItem = accountList.selectedItem; + is(accountListItem.getAttribute("value"), accountKey); + is( + accountListItem.querySelector(".typeIcon:not(.configuredWarning)").src, + "chrome://messenger/content/extension.svg" + ); + is(accountListItem.querySelector("label").value, "Mochitest"); + is(accountListItem.querySelector(".configuredWarning").hidden, false); + + ok(cloudFileDefaultPanel.hidden); + is(browserWrapper.childElementCount, 1); + + let browser = browserWrapper.firstElementChild; + if ( + browser.webProgress?.isLoadingDocument || + browser.currentURI?.spec == "about:blank" + ) { + await BrowserTestUtils.browserLoaded(browser); + } + is( + browser.currentURI.pathQueryRef, + `/management.html?accountId=${accountKey}` + ); + await extension.awaitMessage("management-ui-ready"); + + let tabmail = document.getElementById("tabmail"); + let tabCount = tabmail.tabInfo.length; + BrowserTestUtils.synthesizeMouseAtCenter("a", {}, browser); + // It might take a moment to get to the external protocol service. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 500)); + ok( + mockExternalProtocolService.urlLoaded("https://www.example.com/"), + "Link click sent to external protocol service." + ); + is(tabmail.tabInfo.length, tabCount, "No new tab opened"); + + // Rename the account. + + EventUtils.synthesizeMouseAtCenter( + accountListItem, + { clickCount: 1 }, + prefsWindow + ); + + await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve)); + + is( + prefsDocument.activeElement.closest("input"), + accountListItem.querySelector("input") + ); + ok(accountListItem.querySelector("label").hidden); + ok(!accountListItem.querySelector("input").hidden); + is(accountListItem.querySelector("input").value, "Mochitest"); + EventUtils.synthesizeKey("VK_RIGHT", undefined, prefsWindow); + EventUtils.synthesizeKey("!", undefined, prefsWindow); + EventUtils.synthesizeKey("VK_RETURN", undefined, prefsWindow); + + await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve)); + + is(prefsDocument.activeElement, accountList); + ok(!accountListItem.querySelector("label").hidden); + is(accountListItem.querySelector("label").value, "Mochitest!"); + ok(accountListItem.querySelector("input").hidden); + is( + Services.prefs.getCharPref( + `mail.cloud_files.accounts.${accountKey}.displayName` + ), + "Mochitest!" + ); + + // Start to rename the account, but bail out. + + EventUtils.synthesizeMouseAtCenter( + accountListItem, + { clickCount: 1 }, + prefsWindow + ); + + await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve)); + + is( + prefsDocument.activeElement.closest("input"), + accountListItem.querySelector("input") + ); + EventUtils.synthesizeKey("O", undefined, prefsWindow); + EventUtils.synthesizeKey("o", undefined, prefsWindow); + EventUtils.synthesizeKey("p", undefined, prefsWindow); + EventUtils.synthesizeKey("s", undefined, prefsWindow); + EventUtils.synthesizeKey("VK_ESCAPE", undefined, prefsWindow); + + await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve)); + + is(prefsDocument.activeElement, accountList); + ok(!accountListItem.querySelector("label").hidden); + is(accountListItem.querySelector("label").value, "Mochitest!"); + ok(accountListItem.querySelector("input").hidden); + is( + Services.prefs.getCharPref( + `mail.cloud_files.accounts.${accountKey}.displayName` + ), + "Mochitest!" + ); + + // Configure the account. + + account.configured = true; + accountIsConfigured = true; + cloudFileAccounts.emit("accountConfigured", account); + + await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve)); + + is(accountListItem.querySelector(".configuredWarning").hidden, true); + is(cloudFileAccounts.accounts.length, 1); + is(cloudFileAccounts.configuredAccounts.length, 1); + + // Remove the test provider. The list item, button, and browser should disappear. + + info("Stopping extension"); + await extension.unload(); + is(cloudFileAccounts.providers.length, 1); + is(cloudFileAccounts.accounts.length, 0); + + await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve)); + + is(buttonList.childElementCount, 1); + is( + buttonList.children[0].getAttribute("value"), + "ext-fake@extensions.thunderbird.net" + ); + is(menuButton.itemCount, 1); + is( + menuButton.getItemAtIndex(0).getAttribute("value"), + "ext-fake@extensions.thunderbird.net" + ); + is(accountList.itemCount, 0); + ok(!cloudFileDefaultPanel.hidden); + is(browserWrapper.childElementCount, 0); + + // Re-add the test provider. + + await startExtension(); + + is(cloudFileAccounts.providers.length, 2); + is(cloudFileAccounts.accounts.length, 1); + is(cloudFileAccounts.configuredAccounts.length, 1); + + await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve)); + + is(buttonList.childElementCount, 2); + is( + buttonList.children[0].getAttribute("value"), + "ext-fake@extensions.thunderbird.net" + ); + is(buttonList.children[1].getAttribute("value"), "ext-cloudfile@mochitest"); + + is(menuButton.itemCount, 2); + is( + menuButton.getItemAtIndex(0).getAttribute("value"), + "ext-fake@extensions.thunderbird.net" + ); + is( + menuButton.getItemAtIndex(1).getAttribute("value"), + "ext-cloudfile@mochitest" + ); + + is(accountList.itemCount, 1); + is(accountList.selectedIndex, -1); + ok(removeButton.disabled); + + accountListItem = accountList.getItemAtIndex(0); + is( + Services.prefs.getCharPref( + `mail.cloud_files.accounts.${accountKey}.displayName` + ), + "Mochitest!" + ); + + EventUtils.synthesizeMouseAtCenter( + accountList.getItemAtIndex(0), + { clickCount: 1 }, + prefsWindow + ); + ok(!removeButton.disabled); + EventUtils.synthesizeMouseAtCenter( + removeButton, + { clickCount: 1 }, + prefsWindow + ); + is(mockPromptService.confirmCount, 1); + + ok( + !Services.prefs.prefHasUserValue( + `mail.cloud_files.accounts.${accountKey}.displayName` + ) + ); + ok( + !Services.prefs.prefHasUserValue( + `mail.cloud_files.accounts.${accountKey}.type` + ) + ); + + is(cloudFileAccounts.providers.length, 2); + is(cloudFileAccounts.accounts.length, 0); + + info("Stopping extension"); + await extension.unload(); + is(cloudFileAccounts.providers.length, 1); + is(cloudFileAccounts.accounts.length, 0); + + // Close the preferences tab. + + await closePrefsTab(); +}); + +async function subtestBrowserStyle(assertMessage, expected) { + is(cloudFileAccounts.providers.length, 1); + is(cloudFileAccounts.accounts.length, 0); + + // Load the preferences tab. + + let { prefsDocument, prefsWindow } = await openNewPrefsTab( + "paneCompose", + "compositionAttachmentsCategory" + ); + + // Minimal check everything is as it should be. + + let accountList = prefsDocument.getElementById("cloudFileView"); + is(accountList.itemCount, 0); + + let buttonList = prefsDocument.getElementById("addCloudFileAccountButtons"); + ok(!buttonList.hidden); + + let browserWrapper = prefsDocument.getElementById("cloudFileSettingsWrapper"); + is(browserWrapper.childElementCount, 0); + + // Register our test provider. + + await startExtension(expected.browser_style); + is(cloudFileAccounts.providers.length, 2); + is(cloudFileAccounts.accounts.length, 0); + + await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve)); + + is(buttonList.childElementCount, 2); + is(buttonList.children[1].getAttribute("value"), "ext-cloudfile@mochitest"); + + // Create a new account. + + EventUtils.synthesizeMouseAtCenter( + buttonList.children[1], + { clickCount: 1 }, + prefsWindow + ); + is(cloudFileAccounts.accounts.length, 1); + is(cloudFileAccounts.configuredAccounts.length, 0); + + let account = cloudFileAccounts.accounts[0]; + let accountKey = account.accountKey; + is(cloudFileAccounts.accounts[0].type, "ext-cloudfile@mochitest"); + + // Minimal check UI was updated. + + is(accountList.itemCount, 1); + is(accountList.selectedIndex, 0); + + let accountListItem = accountList.selectedItem; + is(accountListItem.getAttribute("value"), accountKey); + + is(browserWrapper.childElementCount, 1); + let browser = browserWrapper.firstElementChild; + if ( + browser.webProgress?.isLoadingDocument || + browser.currentURI?.spec == "about:blank" + ) { + await BrowserTestUtils.browserLoaded(browser); + } + is( + browser.currentURI.pathQueryRef, + `/management.html?accountId=${accountKey}` + ); + await extension.awaitMessage("management-ui-ready"); + + // Test browser_style + + extension.sendMessage( + "check-style", + assertMessage, + expected.browser_style == "true" + ); + await extension.awaitFinish("management-ui-browser_style"); + + // Remove the account + + accountListItem = accountList.getItemAtIndex(0); + EventUtils.synthesizeMouseAtCenter( + accountList.getItemAtIndex(0), + { clickCount: 1 }, + prefsWindow + ); + + let removeButton = prefsDocument.getElementById("removeCloudFileAccount"); + ok(!removeButton.disabled); + EventUtils.synthesizeMouseAtCenter( + removeButton, + { clickCount: 1 }, + prefsWindow + ); + is(mockPromptService.confirmCount, expected.confirmCount); + + is(cloudFileAccounts.providers.length, 2); + is(cloudFileAccounts.accounts.length, 0); + + info("Stopping extension"); + await extension.unload(); + is(cloudFileAccounts.providers.length, 1); + is(cloudFileAccounts.accounts.length, 0); + + // Close the preferences tab. + + await closePrefsTab(); +} + +add_task(async function test_without_setting_browser_style() { + await subtestBrowserStyle( + "Expected correct style when browser_style is excluded", + { + confirmCount: 2, + browser_style: "default", + } + ); +}); + +add_task(async function test_with_browser_style_set_to_true() { + await subtestBrowserStyle( + "Expected correct style when browser_style is set to `true`", + { + confirmCount: 3, + browser_style: "true", + } + ); +}); + +add_task(async function test_with_browser_style_set_to_false() { + await subtestBrowserStyle( + "Expected no style when browser_style is set to `false`", + { + confirmCount: 4, + browser_style: "false", + } + ); +}); + +add_task(async function accountListOverflow() { + is(cloudFileAccounts.providers.length, 1); + is(cloudFileAccounts.accounts.length, 0); + + // Register our test provider. + + await startExtension(); + + is(cloudFileAccounts.providers.length, 2); + is(cloudFileAccounts.accounts.length, 0); + + // Load the preferences tab. + + let { prefsDocument, prefsWindow } = await openNewPrefsTab( + "paneCompose", + "compositionAttachmentsCategory" + ); + + let accountList = prefsDocument.getElementById("cloudFileView"); + is(accountList.itemCount, 0); + + let buttonList = prefsDocument.getElementById("addCloudFileAccountButtons"); + ok(!buttonList.hidden); + is(buttonList.childElementCount, 2); + is(buttonList.children[0].getAttribute("value"), "ext-cloudfile@mochitest"); + + let menuButton = prefsDocument.getElementById("addCloudFileAccount"); + ok(menuButton.hidden); + + // Add new accounts until the list overflows. The list of buttons should be hidden + // and the button with the drop-down should appear. + + let count = 0; + do { + let readyPromise = extension.awaitMessage("management-ui-ready"); + EventUtils.synthesizeMouseAtCenter( + buttonList.children[0], + { clickCount: 1 }, + prefsWindow + ); + await readyPromise; + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + if (buttonList.hidden) { + break; + } + } while (++count < 25); + + ok(count < 24); // If count reaches 25, we have a problem. + ok(!menuButton.hidden); + + // Remove the added accounts. The list of buttons should not reappear and the + // button with the drop-down should remain. + + let removeButton = prefsDocument.getElementById("removeCloudFileAccount"); + do { + EventUtils.synthesizeMouseAtCenter( + accountList.getItemAtIndex(0), + { clickCount: 1 }, + prefsWindow + ); + EventUtils.synthesizeMouseAtCenter( + removeButton, + { clickCount: 1 }, + prefsWindow + ); + await new Promise(resolve => setTimeout(resolve)); + } while (--count > 0); + + ok(buttonList.hidden); + ok(!menuButton.hidden); + + // Close the preferences tab. + + await closePrefsTab(); + info("Stopping extension"); + await extension.unload(); + Services.prefs.deleteBranch("mail.cloud_files.accounts"); +}); + +add_task(async function accountListOrder() { + is(cloudFileAccounts.providers.length, 1); + is(cloudFileAccounts.accounts.length, 0); + + for (let [key, displayName] of [ + ["someKey1", "carl's Account"], + ["someKey2", "Amber's Account"], + ["someKey3", "alice's Account"], + ["someKey4", "Bob's Account"], + ]) { + Services.prefs.setCharPref( + `mail.cloud_files.accounts.${key}.type`, + "ext-cloudfile@mochitest" + ); + Services.prefs.setCharPref( + `mail.cloud_files.accounts.${key}.displayName`, + displayName + ); + } + + // Register our test provider. + + await startExtension(); + + is(cloudFileAccounts.providers.length, 2); + is(cloudFileAccounts.accounts.length, 4); + + let { prefsDocument } = await openNewPrefsTab( + "paneCompose", + "compositionAttachmentsCategory" + ); + + let accountList = prefsDocument.getElementById("cloudFileView"); + is(accountList.itemCount, 4); + + is(accountList.getItemAtIndex(0).value, "someKey3"); + is(accountList.getItemAtIndex(1).value, "someKey2"); + is(accountList.getItemAtIndex(2).value, "someKey4"); + is(accountList.getItemAtIndex(3).value, "someKey1"); + + await closePrefsTab(); + info("Stopping extension"); + await extension.unload(); + Services.prefs.deleteBranch("mail.cloud_files.accounts"); +}); diff --git a/comm/mail/components/preferences/test/browser/browser_compose.js b/comm/mail/components/preferences/test/browser/browser_compose.js new file mode 100644 index 0000000000..ea253cb555 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/browser_compose.js @@ -0,0 +1,87 @@ +/* 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/. */ + +add_task(async () => { + await testCheckboxes( + "paneCompose", + "compositionMainCategory", + { + checkboxID: "addExtension", + pref: "mail.forward_add_extension", + }, + { + checkboxID: "autoSave", + pref: "mail.compose.autosave", + enabledElements: ["#autoSaveInterval"], + }, + { + checkboxID: "mailWarnOnSendAccelKey", + pref: "mail.warn_on_send_accel_key", + }, + { + checkboxID: "spellCheckBeforeSend", + pref: "mail.SpellCheckBeforeSend", + }, + { + checkboxID: "inlineSpellCheck", + pref: "mail.spellcheck.inline", + } + ); + + await testCheckboxes( + "paneCompose", + "FontSelect", + { + checkboxID: "useReaderDefaults", + pref: "msgcompose.default_colors", + enabledInverted: true, + enabledElements: [ + "#textColorLabel", + "#textColorButton", + "#backgroundColorLabel", + "#backgroundColorButton", + ], + }, + { + checkboxID: "defaultToParagraph", + pref: "mail.compose.default_to_paragraph", + } + ); + + await testCheckboxes( + "paneCompose", + "compositionAddressingCategory", + { + checkboxID: "addressingAutocomplete", + pref: "mail.enable_autocomplete", + }, + { + checkboxID: "autocompleteLDAP", + pref: "ldap_2.autoComplete.useDirectory", + enabledElements: ["#directoriesList", "#editButton"], + }, + { + checkboxID: "emailCollectionOutgoing", + pref: "mail.collect_email_address_outgoing", + enabledElements: ["#localDirectoriesList"], + } + ); +}); + +add_task(async () => { + await testCheckboxes( + "paneCompose", + "compositionAttachmentsCategory", + { + checkboxID: "attachment_reminder_label", + pref: "mail.compose.attachment_reminder", + enabledElements: ["#attachment_reminder_button"], + }, + { + checkboxID: "enableThreshold", + pref: "mail.compose.big_attachments.notify", + enabledElements: ["#cloudFileThreshold"], + } + ); +}); diff --git a/comm/mail/components/preferences/test/browser/browser_general.js b/comm/mail/components/preferences/test/browser/browser_general.js new file mode 100644 index 0000000000..f011be1e38 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/browser_general.js @@ -0,0 +1,380 @@ +/* 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/. */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +add_task(async () => { + requestLongerTimeout(2); + + // Temporarily disable `Once` StaticPrefs check for this test so that we + // can change layers.acceleration.disabled without debug builds failing. + await SpecialPowers.pushPrefEnv({ + set: [["preferences.force-disable.check.once.policy", true]], + }); +}); + +add_task(async () => { + await testCheckboxes( + "paneGeneral", + "generalCategory", + { + checkboxID: "mailnewsStartPageEnabled", + pref: "mailnews.start_page.enabled", + enabledElements: [ + "#mailnewsStartPageUrl", + "#mailnewsStartPageUrl + button", + ], + }, + { + checkboxID: "alwaysCheckDefault", + pref: "mail.shell.checkDefaultClient", + } + ); +}); + +add_task(async () => { + await testCheckboxes( + "paneGeneral", + "scrollingGroup", + { + checkboxID: "useAutoScroll", + pref: "general.autoScroll", + }, + { + checkboxID: "useSmoothScrolling", + pref: "general.smoothScroll", + } + ); +}); + +add_task(async () => { + await testCheckboxes( + "paneGeneral", + "enableGloda", + { + checkboxID: "enableGloda", + pref: "mailnews.database.global.indexer.enabled", + }, + { + checkboxID: "allowHWAccel", + pref: "layers.acceleration.disabled", + prefValues: [true, false], + } + ); +}); + +add_task(async () => { + if (AppConstants.platform != "macosx") { + await testCheckboxes( + "paneGeneral", + "incomingMailCategory", + { + checkboxID: "newMailNotification", + pref: "mail.biff.play_sound", + enabledElements: ["#soundType radio"], + }, + { + checkboxID: "newMailNotificationAlert", + pref: "mail.biff.show_alert", + enabledElements: ["#customizeMailAlert"], + } + ); + } +}); + +add_task(async () => { + if (AppConstants.platform == "macosx") { + return; + } + + Services.prefs.setBoolPref("mail.biff.play_sound", true); + + await testRadioButtons("paneGeneral", "incomingMailCategory", { + pref: "mail.biff.play_sound.type", + states: [ + { + id: "system", + prefValue: 0, + }, + { + id: "custom", + prefValue: 1, + enabledElements: ["#soundUrlLocation", "#browseForSound"], + }, + ], + }); +}); + +add_task(async () => { + await testCheckboxes("paneGeneral", "fontsGroup", { + checkboxID: "displayGlyph", + pref: "mail.display_glyph", + }); + + await testCheckboxes( + "paneGeneral", + "readingAndDisplayCategory", + { + checkboxID: "automaticallyMarkAsRead", + pref: "mailnews.mark_message_read.auto", + enabledElements: ["#markAsReadAutoPreferences radio"], + }, + { + checkboxID: "closeMsgOnMoveOrDelete", + pref: "mail.close_message_window.on_delete", + }, + { + checkboxID: "showCondensedAddresses", + pref: "mail.showCondensedAddresses", + } + ); +}); + +add_task(async () => { + Services.prefs.setBoolPref("mailnews.mark_message_read.auto", true); + + await testRadioButtons( + "paneGeneral", + "mark_read_immediately", + { + pref: "mailnews.mark_message_read.delay", + states: [ + { + id: "mark_read_immediately", + prefValue: false, + }, + { + id: "markAsReadAfterDelay", + prefValue: true, + enabledElements: ["#markAsReadDelay"], + }, + ], + }, + { + pref: "mail.openMessageBehavior", + states: [ + { + id: "newTab", + prefValue: 2, + }, + { + id: "newWindow", + prefValue: 0, + }, + { + id: "existingWindow", + prefValue: 1, + }, + ], + } + ); +}); + +add_task(async () => { + // We don't want to wake up the platform search for this test. + // if (AppConstants.platform == "macosx") { + // tests.push({ + // checkboxID: "searchIntegration", + // pref: "mail.spotlight.enable", + // }); + // } else if (AppConstants.platform == "win") { + // tests.push({ + // checkboxID: "searchIntegration", + // pref: "mail.winsearch.enable", + // }); + // } + + await testCheckboxes( + "paneGeneral", + "allowSmartSize", + { + checkboxID: "allowSmartSize", + pref: "browser.cache.disk.smart_size.enabled", + prefValues: [true, false], + enabledElements: ["#cacheSize"], + }, + { + checkboxID: "offlineCompactFolder", + pref: "mail.prompt_purge_threshhold", + enabledElements: [ + "#offlineCompactFolderMin", + "#offlineCompactFolderAutomatically", + ], + } + ); +}); + +add_task(async () => { + await testRadioButtons("paneGeneral", "formatLocale", { + pref: "intl.regional_prefs.use_os_locales", + states: [ + { + id: "appLocale", + prefValue: false, + }, + { + id: "rsLocale", + prefValue: true, + }, + ], + }); +}); + +add_task(async () => { + await testRadioButtons("paneGeneral", "filesAttachmentCategory", { + pref: "browser.download.useDownloadDir", + states: [ + { + id: "saveTo", + prefValue: true, + enabledElements: ["#downloadFolder", "#chooseFolder"], + }, + { + id: "alwaysAsk", + prefValue: false, + }, + ], + }); +}); + +add_task(async function testTagDialog() { + const { prefsDocument, prefsWindow } = await openNewPrefsTab( + "paneGeneral", + "tagsCategory" + ); + + let newTagDialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + undefined, + "chrome://messenger/content/preferences/tagDialog.xhtml", + { + isSubDialog: true, + async callback(dialogWindow) { + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow == dialogWindow, + "waiting for subdialog to be focused" + ); + + const dialogDocument = dialogWindow.document; + + EventUtils.sendString("tbird", dialogWindow); + // "#000080" == rgb(0, 0, 128); + dialogDocument.getElementById("tagColorPicker").value = "#000080"; + + EventUtils.synthesizeMouseAtCenter( + dialogDocument.querySelector("dialog").getButton("accept"), + {}, + dialogWindow + ); + await new Promise(r => setTimeout(r)); + }, + } + ); + + let newTagButton = prefsDocument.getElementById("newTagButton"); + EventUtils.synthesizeMouseAtCenter(newTagButton, {}, prefsWindow); + await newTagDialogPromise; + + let tagList = prefsDocument.getElementById("tagList"); + + Assert.ok( + tagList.querySelector('richlistitem[value="tbird"]'), + "new tbird tag should be in the list" + ); + Assert.equal( + tagList.querySelector('richlistitem[value="tbird"]').style.color, + "rgb(0, 0, 128)", + "tbird tag color should be correct" + ); + Assert.equal( + tagList.querySelectorAll('richlistitem[value="tbird"]').length, + 1, + "new tbird tag should be in the list exactly once" + ); + + Assert.equal( + tagList.querySelector('richlistitem[value="tbird"]'), + tagList.selectedItem, + "tbird tag should be selected" + ); + + // Now edit the tag. The key should stay the same, name and color will change. + + let editTagDialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + undefined, + "chrome://messenger/content/preferences/tagDialog.xhtml", + { + isSubDialog: true, + async callback(dialogWindow) { + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow == dialogWindow, + "waiting for subdialog to be focused" + ); + + const dialogDocument = dialogWindow.document; + + Assert.equal( + dialogDocument.getElementById("name").value, + "tbird", + "should have existing tbird tag name prefilled" + ); + Assert.equal( + dialogDocument.getElementById("tagColorPicker").value, + "#000080", + "should have existing tbird tag color prefilled" + ); + + EventUtils.sendString("-xx", dialogWindow); // => tbird-xx + // "#FFD700" == rgb(255, 215, 0); + dialogDocument.getElementById("tagColorPicker").value = "#FFD700"; + + EventUtils.synthesizeMouseAtCenter( + dialogDocument.querySelector("dialog").getButton("accept"), + {}, + dialogWindow + ); + await new Promise(r => setTimeout(r)); + }, + } + ); + + let editTagButton = prefsDocument.getElementById("editTagButton"); + EventUtils.synthesizeMouseAtCenter(editTagButton, {}, prefsWindow); + await editTagDialogPromise; + + Assert.ok( + tagList.querySelector( + 'richlistitem[value="tbird"] > label[value="tbird-xx"]' + ), + "tbird-xx tag should be in the list" + ); + Assert.equal( + tagList.querySelector('richlistitem[value="tbird"]').style.color, + "rgb(255, 215, 0)", + "tbird-xx tag color should be correct" + ); + Assert.equal( + tagList.querySelectorAll('richlistitem[value="tbird"]').length, + 1, + "tbird-xx tag should be in the list exactly once" + ); + + // And remove it. + + EventUtils.synthesizeMouseAtCenter( + prefsDocument.getElementById("removeTagButton"), + {}, + prefsWindow + ); + await new Promise(r => setTimeout(r)); + + Assert.equal( + tagList.querySelector('richlistitem[value="tbird"]'), + null, + "tbird-xx (with key tbird) tag should have been removed from the list" + ); + + await closePrefsTab(); +}); diff --git a/comm/mail/components/preferences/test/browser/browser_openPreferences.js b/comm/mail/components/preferences/test/browser/browser_openPreferences.js new file mode 100644 index 0000000000..01aceb085a --- /dev/null +++ b/comm/mail/components/preferences/test/browser/browser_openPreferences.js @@ -0,0 +1,37 @@ +/* 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/. */ + +function getStoredLastSelected() { + return Services.xulStore.getValue( + "about:preferences", + "paneDeck", + "lastSelected" + ); +} + +add_task(async () => { + // Check that openPreferencesTab with no arguments and no stored value opens the first pane. + Services.xulStore.removeDocument("about:preferences"); + + let { prefsWindow } = await openNewPrefsTab(); + is(prefsWindow.gLastCategory.category, "paneGeneral"); + + await closePrefsTab(); +}); + +add_task(async () => { + // Check that openPreferencesTab with one argument opens the right pane… + Services.xulStore.removeDocument("about:preferences"); + + await openNewPrefsTab("panePrivacy"); + is(getStoredLastSelected(), "panePrivacy"); + + await closePrefsTab(); + + // … even with a value in the XULStore. + await openNewPrefsTab("paneCompose"); + is(getStoredLastSelected(), "paneCompose"); + + await closePrefsTab(); +}); diff --git a/comm/mail/components/preferences/test/browser/browser_privacy.js b/comm/mail/components/preferences/test/browser/browser_privacy.js new file mode 100644 index 0000000000..1b91ec35e8 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/browser_privacy.js @@ -0,0 +1,454 @@ +/* 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/. */ + +add_task(async () => { + await testCheckboxes( + "panePrivacy", + "privacyCategory", + { + checkboxID: "acceptRemoteContent", + pref: "mailnews.message_display.disable_remote_image", + prefValues: [true, false], + }, + { + checkboxID: "keepHistory", + pref: "places.history.enabled", + }, + { + checkboxID: "acceptCookies", + pref: "network.cookie.cookieBehavior", + prefValues: [2, 0], + enabledElements: ["#acceptThirdPartyMenu"], + unaffectedElements: ["#cookieExceptions"], + }, + { + checkboxID: "privacyDoNotTrackCheckbox", + pref: "privacy.donottrackheader.enabled", + } + ); +}); + +add_task(async () => { + await testCheckboxes( + "panePrivacy", + "privacyJunkCategory", + { + checkboxID: "manualMark", + pref: "mail.spam.manualMark", + enabledElements: ["#manualMarkMode radio"], + }, + { + checkboxID: "markAsReadOnSpam", + pref: "mail.spam.markAsReadOnSpam", + }, + { + checkboxID: "enableJunkLogging", + pref: "mail.spam.logging.enabled", + enabledElements: ["#openJunkLogButton"], + } + ); + + await testCheckboxes("panePrivacy", "privacySecurityCategory", { + checkboxID: "enablePhishingDetector", + pref: "mail.phishing.detection.enabled", + }); + + await testCheckboxes("panePrivacy", "enableAntiVirusQuarantine", { + checkboxID: "enableAntiVirusQuarantine", + pref: "mailnews.downloadToTempFile", + }); +}); + +add_task(async () => { + Services.prefs.setBoolPref("mail.spam.manualMark", true); + + await testRadioButtons("panePrivacy", "privacyJunkCategory", { + pref: "mail.spam.manualMarkMode", + states: [ + { + id: "manualMarkMode0", + prefValue: 0, + }, + { + id: "manualMarkMode1", + prefValue: 1, + }, + ], + }); +}); + +add_task(async () => { + // Telemetry pref is locked. + // await testCheckboxes("paneAdvanced", undefined, { + // checkboxID: "submitTelemetryBox", + // pref: "toolkit.telemetry.enabled", + // }); + + await testCheckboxes("panePrivacy", "enableOCSP", { + checkboxID: "enableOCSP", + pref: "security.OCSP.enabled", + prefValues: [0, 1], + }); +}); + +// Here we'd test the update choices, but I don't want to go near that. +add_task(async () => { + await testRadioButtons("panePrivacy", "enableOCSP", { + pref: "security.default_personal_cert", + states: [ + { + id: "certSelectionAuto", + prefValue: "Select Automatically", + }, + { + id: "certSelectionAsk", + prefValue: "Ask Every Time", + }, + ], + }); +}); + +add_task(async function testRemoteContentDialog() { + const { prefsDocument, prefsWindow } = await openNewPrefsTab("panePrivacy"); + + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + undefined, + "chrome://messenger/content/preferences/permissions.xhtml", + { + isSubDialog: true, + async callback(dialogWindow) { + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow == dialogWindow, + "waiting for subdialog to be focused" + ); + + const dialogDocument = dialogWindow.document; + const url = dialogDocument.getElementById("url"); + const permissionsTree = + dialogDocument.getElementById("permissionsTree"); + + EventUtils.sendString("accept.invalid", dialogWindow); + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("btnAllow"), + {}, + dialogWindow + ); + await new Promise(f => setTimeout(f)); + Assert.equal(url.value, "", "url input should be cleared"); + Assert.equal( + permissionsTree.view.rowCount, + 1, + "new entry should be added to list" + ); + + Assert.ok( + BrowserTestUtils.is_hidden( + dialogDocument.getElementById("btnSession") + ), + "session button should be hidden" + ); + + EventUtils.sendString("block.invalid", dialogWindow); + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("btnBlock"), + {}, + dialogWindow + ); + await new Promise(f => setTimeout(f)); + Assert.equal(url.value, "", "url input should be cleared"); + Assert.equal( + permissionsTree.view.rowCount, + 2, + "new entry should be added to list" + ); + + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("btnApplyChanges"), + {}, + dialogWindow + ); + }, + } + ); + let remoteContentExceptions = prefsDocument.getElementById( + "remoteContentExceptions" + ); + EventUtils.synthesizeMouseAtCenter(remoteContentExceptions, {}, prefsWindow); + await dialogPromise; + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let acceptURI = Services.io.newURI("http://accept.invalid/"); + let acceptPrincipal = Services.scriptSecurityManager.createContentPrincipal( + acceptURI, + {} + ); + Assert.equal( + Services.perms.testPermissionFromPrincipal(acceptPrincipal, "image"), + Ci.nsIPermissionManager.ALLOW_ACTION, + "accept permission should exist for accept.invalid" + ); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let blockURI = Services.io.newURI("http://block.invalid/"); + let blockPrincipal = Services.scriptSecurityManager.createContentPrincipal( + blockURI, + {} + ); + Assert.equal( + Services.perms.testPermissionFromPrincipal(blockPrincipal, "image"), + Ci.nsIPermissionManager.DENY_ACTION, + "block permission should exist for block.invalid" + ); + + dialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + undefined, + "chrome://messenger/content/preferences/permissions.xhtml", + { + isSubDialog: true, + async callback(dialogWindow) { + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow == dialogWindow, + "waiting for subdialog to be focused" + ); + + const dialogDocument = dialogWindow.document; + const permissionsTree = + dialogDocument.getElementById("permissionsTree"); + + Assert.equal( + permissionsTree.view.rowCount, + 2, + "list should be populated" + ); + + permissionsTree.view.selection.select(0); + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("removePermission"), + {}, + dialogWindow + ); + Assert.equal( + permissionsTree.view.rowCount, + 1, + "row should be removed from list" + ); + + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("removeAllPermissions"), + {}, + dialogWindow + ); + Assert.equal( + permissionsTree.view.rowCount, + 0, + "row should be removed from list" + ); + + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("btnApplyChanges"), + {}, + dialogWindow + ); + }, + } + ); + EventUtils.synthesizeMouseAtCenter(remoteContentExceptions, {}, prefsWindow); + await dialogPromise; + + Assert.equal( + Services.perms.testPermissionFromPrincipal(acceptPrincipal, "image"), + Ci.nsIPermissionManager.UNKNOWN_ACTION, + "permission should be removed for accept.invalid" + ); + Assert.equal( + Services.perms.testPermissionFromPrincipal(blockPrincipal, "image"), + Ci.nsIPermissionManager.UNKNOWN_ACTION, + "permission should be removed for block.invalid" + ); + + await closePrefsTab(); +}); + +add_task(async function testCookiesDialog() { + const { prefsDocument, prefsWindow } = await openNewPrefsTab("panePrivacy"); + + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + undefined, + "chrome://messenger/content/preferences/permissions.xhtml", + { + isSubDialog: true, + async callback(dialogWindow) { + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow == dialogWindow, + "waiting for subdialog to be focused" + ); + + const dialogDocument = dialogWindow.document; + const url = dialogDocument.getElementById("url"); + const permissionsTree = + dialogDocument.getElementById("permissionsTree"); + + EventUtils.sendString("accept.invalid", dialogWindow); + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("btnAllow"), + {}, + dialogWindow + ); + await new Promise(f => setTimeout(f)); + Assert.equal(url.value, "", "url input should be cleared"); + Assert.equal( + permissionsTree.view.rowCount, + 1, + "new entry should be added to list" + ); + + EventUtils.sendString("session.invalid", dialogWindow); + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("btnSession"), + {}, + dialogWindow + ); + await new Promise(f => setTimeout(f)); + Assert.equal(url.value, "", "url input should be cleared"); + Assert.equal( + permissionsTree.view.rowCount, + 2, + "new entry should be added to list" + ); + + EventUtils.sendString("block.invalid", dialogWindow); + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("btnBlock"), + {}, + dialogWindow + ); + await new Promise(f => setTimeout(f)); + Assert.equal(url.value, "", "url input should be cleared"); + Assert.equal( + permissionsTree.view.rowCount, + 3, + "new entry should be added to list" + ); + + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("btnApplyChanges"), + {}, + dialogWindow + ); + }, + } + ); + let cookieExceptions = prefsDocument.getElementById("cookieExceptions"); + EventUtils.synthesizeMouseAtCenter(cookieExceptions, {}, prefsWindow); + await dialogPromise; + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let acceptURI = Services.io.newURI("http://accept.invalid/"); + let acceptPrincipal = Services.scriptSecurityManager.createContentPrincipal( + acceptURI, + {} + ); + Assert.equal( + Services.perms.testPermissionFromPrincipal(acceptPrincipal, "cookie"), + Ci.nsIPermissionManager.ALLOW_ACTION, + "accept permission should exist for accept.invalid" + ); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let sessionURI = Services.io.newURI("http://session.invalid/"); + let sessionPrincipal = Services.scriptSecurityManager.createContentPrincipal( + sessionURI, + {} + ); + Assert.equal( + Services.perms.testPermissionFromPrincipal(sessionPrincipal, "cookie"), + Ci.nsICookiePermission.ACCESS_SESSION, + "session permission should exist for session.invalid" + ); + + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + let blockURI = Services.io.newURI("http://block.invalid/"); + let blockPrincipal = Services.scriptSecurityManager.createContentPrincipal( + blockURI, + {} + ); + Assert.equal( + Services.perms.testPermissionFromPrincipal(blockPrincipal, "cookie"), + Ci.nsIPermissionManager.DENY_ACTION, + "block permission should exist for block.invalid" + ); + + dialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + undefined, + "chrome://messenger/content/preferences/permissions.xhtml", + { + isSubDialog: true, + async callback(dialogWindow) { + await TestUtils.waitForCondition( + () => Services.focus.focusedWindow == dialogWindow, + "waiting for subdialog to be focused" + ); + + const dialogDocument = dialogWindow.document; + const permissionsTree = + dialogDocument.getElementById("permissionsTree"); + + Assert.equal( + permissionsTree.view.rowCount, + 3, + "list should be populated" + ); + + permissionsTree.view.selection.select(0); + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("removePermission"), + {}, + dialogWindow + ); + Assert.equal( + permissionsTree.view.rowCount, + 2, + "row should be removed from list" + ); + + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("removeAllPermissions"), + {}, + dialogWindow + ); + Assert.equal( + permissionsTree.view.rowCount, + 0, + "row should be removed from list" + ); + + EventUtils.synthesizeMouseAtCenter( + dialogDocument.getElementById("btnApplyChanges"), + {}, + dialogWindow + ); + }, + } + ); + EventUtils.synthesizeMouseAtCenter(cookieExceptions, {}, prefsWindow); + await dialogPromise; + + Assert.equal( + Services.perms.testPermissionFromPrincipal(acceptPrincipal, "cookie"), + Ci.nsIPermissionManager.UNKNOWN_ACTION, + "permission should be removed for accept.invalid" + ); + Assert.equal( + Services.perms.testPermissionFromPrincipal(sessionPrincipal, "cookie"), + Ci.nsIPermissionManager.UNKNOWN_ACTION, + "permission should be removed for session.invalid" + ); + Assert.equal( + Services.perms.testPermissionFromPrincipal(blockPrincipal, "cookie"), + Ci.nsIPermissionManager.UNKNOWN_ACTION, + "permission should be removed for block.invalid" + ); + + await closePrefsTab(); +}); diff --git a/comm/mail/components/preferences/test/browser/browser_sync.js b/comm/mail/components/preferences/test/browser/browser_sync.js new file mode 100644 index 0000000000..108695ebb5 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/browser_sync.js @@ -0,0 +1,419 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { MockRegistrar } = ChromeUtils.importESModule( + "resource://testing-common/MockRegistrar.sys.mjs" +); + +const { FxAccounts } = ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" +); +FxAccounts.config.promiseConnectAccountURI = entryPoint => + `https://example.org/?page=connect&entryPoint=${entryPoint}`; +FxAccounts.config.promiseManageURI = entryPoint => + `https://example.org/?page=manage&entryPoint=${entryPoint}`; +FxAccounts.config.promiseChangeAvatarURI = entryPoint => + `https://example.org/?page=avatar&entryPoint=${entryPoint}`; + +const ALL_ENGINES = [ + "accounts", + "identities", + "addressbooks", + "calendars", + "passwords", +]; +const PREF_PREFIX = "services.sync.engine"; + +let prefsWindow, prefsDocument, tabmail; + +add_setup(async function () { + for (let engine of ALL_ENGINES) { + Services.prefs.setBoolPref(`${PREF_PREFIX}.${engine}`, true); + } + + ({ prefsWindow, prefsDocument } = await openNewPrefsTab("paneSync")); + tabmail = document.getElementById("tabmail"); + + /** @implements {nsIExternalProtocolService} */ + let mockExternalProtocolService = { + QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]), + externalProtocolHandlerExists(protocolScheme) {}, + isExposedProtocol(protocolScheme) {}, + loadURI(uri, windowContext) { + Assert.report( + true, + undefined, + undefined, + `should not be opening ${uri.spec} in an external browser` + ); + }, + }; + + let mockExternalProtocolServiceCID = MockRegistrar.register( + "@mozilla.org/uriloader/external-protocol-service;1", + mockExternalProtocolService + ); + registerCleanupFunction(() => { + MockRegistrar.unregister(mockExternalProtocolServiceCID); + }); +}); + +add_task(async function testSectionStates() { + let noFxaAccount = prefsDocument.getElementById("noFxaAccount"); + let hasFxaAccount = prefsDocument.getElementById("hasFxaAccount"); + let accountStates = [noFxaAccount, hasFxaAccount]; + + let fxaLoginUnverified = prefsDocument.getElementById("fxaLoginUnverified"); + let fxaLoginRejected = prefsDocument.getElementById("fxaLoginRejected"); + let fxaLoginVerified = prefsDocument.getElementById("fxaLoginVerified"); + let loginStates = [fxaLoginUnverified, fxaLoginRejected, fxaLoginVerified]; + + let fxaDeviceInfo = prefsDocument.getElementById("fxaDeviceInfo"); + let syncConnected = prefsDocument.getElementById("syncConnected"); + let syncDisconnected = prefsDocument.getElementById("syncDisconnected"); + let syncStates = [syncConnected, syncDisconnected]; + + function assertStateVisible(states, visibleState) { + for (let state of states) { + let visible = BrowserTestUtils.is_visible(state); + Assert.equal( + visible, + state == visibleState, + `${state.id} should be ${state == visibleState ? "visible" : "hidden"}` + ); + } + } + + function checkStates({ + accountState, + loginState = null, + deviceInfoVisible = false, + syncState = null, + }) { + prefsWindow.gSyncPane.updateWeavePrefs(); + assertStateVisible(accountStates, accountState); + assertStateVisible(loginStates, loginState); + Assert.equal( + BrowserTestUtils.is_visible(fxaDeviceInfo), + deviceInfoVisible, + `fxaDeviceInfo should be ${deviceInfoVisible ? "visible" : "hidden"}` + ); + assertStateVisible(syncStates, syncState); + } + + async function assertTabOpens(target, expectedURL) { + if (typeof target == "string") { + target = prefsDocument.getElementById(target); + } + + let tabPromise = BrowserTestUtils.waitForEvent(window, "TabOpen"); + EventUtils.synthesizeMouseAtCenter(target, {}, prefsWindow); + await tabPromise; + let tab = tabmail.currentTabInfo; + await BrowserTestUtils.browserLoaded(tab.browser); + Assert.equal( + tab.browser.currentURI.spec, + `https://example.org/${expectedURL}`, + "a tab opened to the correct URL" + ); + tabmail.closeTab(tab); + } + + info("No account"); + Assert.equal(prefsWindow.UIState.get().status, "not_configured"); + checkStates({ accountState: noFxaAccount }); + + // Check clicking the Sign In button opens the connect page in a tab. + await assertTabOpens("noFxaSignIn", "?page=connect&entryPoint="); + + // Override the window's UIState object with mock values. + let baseState = { + email: "test@invalid", + displayName: "Testy McTest", + avatarURL: + "https://example.org/browser/comm/mail/components/preferences/test/browser/files/avatar.png", + avatarIsDefault: false, + }; + let mockState; + prefsWindow.UIState = { + ON_UPDATE: "sync-ui-state:update", + STATUS_LOGIN_FAILED: "login_failed", + STATUS_NOT_CONFIGURED: "not_configured", + STATUS_NOT_VERIFIED: "not_verified", + STATUS_SIGNED_IN: "signed_in", + get() { + return mockState; + }, + }; + + info("Login not verified"); + mockState = { ...baseState, status: "not_verified" }; + checkStates({ + accountState: hasFxaAccount, + loginState: fxaLoginUnverified, + deviceInfoVisible: true, + }); + Assert.deepEqual( + await prefsDocument.l10n.getAttributes( + prefsDocument.getElementById("fxaAccountMailNotVerified") + ), + { + id: "sync-pane-email-not-verified", + args: { userEmail: "test@invalid" }, + }, + "email address set correctly" + ); + + // Untested: Resend and remove account buttons. + + info("Login rejected"); + mockState = { ...baseState, status: "login_failed" }; + checkStates({ + accountState: hasFxaAccount, + loginState: fxaLoginRejected, + deviceInfoVisible: true, + }); + Assert.deepEqual( + await prefsDocument.l10n.getAttributes( + prefsDocument.getElementById("fxaAccountLoginRejected") + ), + { + id: "sync-signedin-login-failure", + args: { userEmail: "test@invalid" }, + }, + "email address set correctly" + ); + + // Untested: Sign in and remove account buttons. + + info("Logged in, sync disabled"); + mockState = { ...baseState, status: "verified", syncEnabled: false }; + checkStates({ + accountState: hasFxaAccount, + loginState: fxaLoginVerified, + deviceInfoVisible: true, + syncState: syncDisconnected, + }); + let photo = fxaLoginVerified.querySelector(".contact-photo"); + Assert.equal( + photo.src, + "https://example.org/browser/comm/mail/components/preferences/test/browser/files/avatar.png", + "avatar image set correctly" + ); + + // Check clicking the avatar image opens the avatar page in a tab. + await assertTabOpens(photo, "?page=avatar&entryPoint=preferences"); + + Assert.equal( + prefsDocument.getElementById("fxaDisplayName").textContent, + "Testy McTest", + "display name set correctly" + ); + Assert.equal( + prefsDocument.getElementById("fxaEmailAddress").textContent, + "test@invalid", + "email address set correctly" + ); + + // Check clicking the management link opens the management page in a tab. + await assertTabOpens("verifiedManage", "?page=manage&entryPoint=preferences"); + + // Untested: Sign out button. + + info("Device name section"); + let deviceNameInput = prefsDocument.getElementById("fxaDeviceNameInput"); + let deviceNameCancel = prefsDocument.getElementById("fxaDeviceNameCancel"); + let deviceNameSave = prefsDocument.getElementById("fxaDeviceNameSave"); + let deviceNameChange = prefsDocument.getElementById( + "fxaDeviceNameChangeDeviceName" + ); + Assert.ok(deviceNameInput.readOnly, "input is read-only"); + Assert.ok( + BrowserTestUtils.is_hidden(deviceNameCancel), + "cancel button is hidden" + ); + Assert.ok( + BrowserTestUtils.is_hidden(deviceNameSave), + "save button is hidden" + ); + Assert.ok( + BrowserTestUtils.is_visible(deviceNameChange), + "change button is visible" + ); + + EventUtils.synthesizeMouseAtCenter(deviceNameChange, {}, prefsWindow); + Assert.ok(!deviceNameInput.readOnly, "input is writeable"); + Assert.equal(prefsDocument.activeElement, deviceNameInput, "input is active"); + Assert.ok( + BrowserTestUtils.is_visible(deviceNameCancel), + "cancel button is visible" + ); + Assert.ok( + BrowserTestUtils.is_visible(deviceNameSave), + "save button is visible" + ); + Assert.ok( + BrowserTestUtils.is_hidden(deviceNameChange), + "change button is hidden" + ); + + EventUtils.synthesizeMouseAtCenter(deviceNameCancel, {}, prefsWindow); + Assert.ok(deviceNameInput.readOnly, "input is read-only"); + Assert.ok( + BrowserTestUtils.is_hidden(deviceNameCancel), + "cancel button is hidden" + ); + Assert.ok( + BrowserTestUtils.is_hidden(deviceNameSave), + "save button is hidden" + ); + Assert.ok( + BrowserTestUtils.is_visible(deviceNameChange), + "change button is visible" + ); + + // Check the turn on sync button works. + await openEngineDialog({ expectEngines: ALL_ENGINES, button: "syncSetup" }); + + info("Logged in, sync enabled"); + mockState = { ...baseState, status: "verified", syncEnabled: true }; + checkStates({ + accountState: hasFxaAccount, + loginState: fxaLoginVerified, + deviceInfoVisible: true, + syncState: syncConnected, + }); + + // Untested: Sync now button. + + // Check the learn more link opens a tab. + await assertTabOpens("enginesLearnMore", "?page=learnMore"); + + // Untested: Disconnect button. +}); + +add_task(async function testEngines() { + function assertEnginesEnabled(...expectedEnabled) { + for (let engine of ALL_ENGINES) { + let enabled = Services.prefs.getBoolPref(`${PREF_PREFIX}.${engine}`); + Assert.equal( + enabled, + expectedEnabled.includes(engine), + `${engine} should be ${ + expectedEnabled.includes(engine) ? "enabled" : "disabled" + }` + ); + } + } + + function assertEnginesShown(...expectEngines) { + let ENGINES_TO_ITEMS = { + accounts: "showSyncAccount", + identities: "showSyncIdentity", + addressbooks: "showSyncAddress", + calendars: "showSyncCalendar", + passwords: "showSyncPasswords", + }; + let expectItems = expectEngines.map(engine => ENGINES_TO_ITEMS[engine]); + let items = Array.from( + prefsDocument.querySelectorAll("#showSyncedList > li:not([hidden])"), + li => li.id + ); + Assert.deepEqual(items, expectItems, "enabled engines shown correctly"); + } + + assertEnginesShown(...ALL_ENGINES); + Services.prefs.setBoolPref(`${PREF_PREFIX}.accounts`, false); + assertEnginesShown("identities", "addressbooks", "calendars", "passwords"); + Services.prefs.setBoolPref(`${PREF_PREFIX}.identities`, false); + Services.prefs.setBoolPref(`${PREF_PREFIX}.addressbooks`, false); + Services.prefs.setBoolPref(`${PREF_PREFIX}.calendars`, false); + assertEnginesShown("passwords"); + Services.prefs.setBoolPref(`${PREF_PREFIX}.passwords`, false); + assertEnginesShown(); + + info("Checking the engine selection dialog"); + await openEngineDialog({ + toggleEngines: ["accounts", "identities", "passwords"], + }); + + assertEnginesEnabled("accounts", "identities", "passwords"); + assertEnginesShown("accounts", "identities", "passwords"); + + await openEngineDialog({ + expectEngines: ["accounts", "identities", "passwords"], + toggleEngines: ["calendars", "passwords"], + action: "cancel", + }); + + assertEnginesEnabled("accounts", "identities", "passwords"); + assertEnginesShown("accounts", "identities", "passwords"); + + await openEngineDialog({ + expectEngines: ["accounts", "identities", "passwords"], + toggleEngines: ["calendars", "passwords"], + action: "accept", + }); + + assertEnginesEnabled("accounts", "identities", "calendars"); + assertEnginesShown("accounts", "identities", "calendars"); + + Services.prefs.setBoolPref(`${PREF_PREFIX}.addressbooks`, true); + Services.prefs.setBoolPref(`${PREF_PREFIX}.passwords`, true); + assertEnginesShown(...ALL_ENGINES); +}); + +async function openEngineDialog({ + expectEngines = [], + toggleEngines = [], + action = "accept", + button = "syncChangeOptions", +}) { + const ENGINES_TO_CHECKBOXES = { + accounts: "configSyncAccount", + identities: "configSyncIdentity", + addressbooks: "configSyncAddress", + calendars: "configSyncCalendar", + passwords: "configSyncPasswords", + }; + let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen( + undefined, + "chrome://messenger/content/preferences/syncDialog.xhtml", + { isSubDialog: true } + ); + EventUtils.synthesizeMouseAtCenter( + prefsDocument.getElementById(button), + {}, + prefsWindow + ); + let dialogWindow = await dialogPromise; + let dialogDocument = dialogWindow.document; + await new Promise(resolve => dialogWindow.setTimeout(resolve)); + + let expectItems = expectEngines.map(engine => ENGINES_TO_CHECKBOXES[engine]); + + let checkedItems = Array.from( + dialogDocument.querySelectorAll(`input[type="checkbox"]`) + ) + .filter(cb => cb.checked) + .map(cb => cb.id); + Assert.deepEqual( + checkedItems, + expectItems, + "enabled engines checked correctly" + ); + + for (let toggleItem of toggleEngines) { + let checkbox = dialogDocument.getElementById( + ENGINES_TO_CHECKBOXES[toggleItem] + ); + checkbox.checked = !checkbox.checked; + } + + EventUtils.synthesizeMouseAtCenter( + dialogDocument.querySelector("dialog").getButton(action), + {}, + dialogWindow + ); +} diff --git a/comm/mail/components/preferences/test/browser/files/avatar.png b/comm/mail/components/preferences/test/browser/files/avatar.png Binary files differnew file mode 100644 index 0000000000..ca0894316a --- /dev/null +++ b/comm/mail/components/preferences/test/browser/files/avatar.png diff --git a/comm/mail/components/preferences/test/browser/files/icon.svg b/comm/mail/components/preferences/test/browser/files/icon.svg new file mode 100644 index 0000000000..6c1a552445 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/files/icon.svg @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> + <circle cx="8" cy="8" r="7.5" fill="#ffffff" stroke="#00aa00" stroke-width="1.5"/> + <circle cx="5" cy="6" r="1.5" fill="#00aa00"/> + <circle cx="11" cy="6" r="1.5" fill="#00aa00"/> + <path d="M 12.83,9.30 C 12.24,11.48 10.26,13 8,13 5.75,13 3.74,11.48 3.17,9.29" fill="none" stroke="#00aa00" stroke-width="1.5"/> +</svg> diff --git a/comm/mail/components/preferences/test/browser/files/management.html b/comm/mail/components/preferences/test/browser/files/management.html new file mode 100644 index 0000000000..7e3561d823 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/files/management.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"/> + <title></title> +</head> +<body> + <a id="a" href="https://www.example.com/">Click me!</a> +</body> +</html> diff --git a/comm/mail/components/preferences/test/browser/head.js b/comm/mail/components/preferences/test/browser/head.js new file mode 100644 index 0000000000..12cbdb17f1 --- /dev/null +++ b/comm/mail/components/preferences/test/browser/head.js @@ -0,0 +1,314 @@ +/* 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 ../../../../base/content/utilityOverlay.js */ + +async function openNewPrefsTab(paneID, scrollPaneTo, otherArgs) { + let tabmail = document.getElementById("tabmail"); + let prefsTabMode = tabmail.tabModes.preferencesTab; + + is(prefsTabMode.tabs.length, 0, "Prefs tab is not open"); + + let prefsDocument = await new Promise(resolve => { + Services.obs.addObserver(function documentLoaded(subject) { + if (subject.URL.startsWith("about:preferences")) { + Services.obs.removeObserver(documentLoaded, "chrome-document-loaded"); + subject.ownerGlobal.setTimeout(() => resolve(subject)); + } + }, "chrome-document-loaded"); + openPreferencesTab(paneID, scrollPaneTo, otherArgs); + }); + ok(prefsDocument.URL.startsWith("about:preferences"), "Prefs tab is open"); + + prefsDocument = prefsTabMode.tabs[0].browser.contentDocument; + let prefsWindow = prefsDocument.ownerGlobal; + prefsWindow.resizeTo(screen.availWidth, screen.availHeight); + + if (paneID) { + await new Promise(resolve => prefsWindow.setTimeout(resolve)); + is( + prefsWindow.gLastCategory.category, + paneID, + `Selected pane is ${paneID}` + ); + } else { + // If we don't wait here for other scripts to run, they + // could be in a bad state if our test closes the tab. + await new Promise(resolve => prefsWindow.setTimeout(resolve)); + } + + registerCleanupOnce(); + + await new Promise(resolve => prefsWindow.setTimeout(resolve)); + let container = prefsDocument.getElementById("preferencesContainer"); + if (scrollPaneTo && container.scrollHeight > container.clientHeight) { + Assert.greater( + container.scrollTop, + 0, + "Prefs page did scroll when it was supposed to" + ); + } + return { prefsDocument, prefsWindow }; +} + +async function openExistingPrefsTab(paneID, scrollPaneTo, otherArgs) { + let tabmail = document.getElementById("tabmail"); + let prefsTabMode = tabmail.tabModes.preferencesTab; + + is(prefsTabMode.tabs.length, 1, "Prefs tab is open"); + + let prefsDocument = prefsTabMode.tabs[0].browser.contentDocument; + let prefsWindow = prefsDocument.ownerGlobal; + prefsWindow.resizeTo(screen.availWidth, screen.availHeight); + + if (paneID && prefsWindow.gLastCategory.category != paneID) { + openPreferencesTab(paneID, scrollPaneTo, otherArgs); + } + + await new Promise(resolve => prefsWindow.setTimeout(resolve)); + is(prefsWindow.gLastCategory.category, paneID, `Selected pane is ${paneID}`); + + if (scrollPaneTo) { + Assert.greater( + prefsDocument.getElementById("preferencesContainer").scrollTop, + 0, + "Prefs page did scroll when it was supposed to" + ); + } + + registerCleanupOnce(); + return { prefsDocument, prefsWindow }; +} + +function registerCleanupOnce() { + if (registerCleanupOnce.alreadyRegistered) { + return; + } + registerCleanupFunction(closePrefsTab); + registerCleanupOnce.alreadyRegistered = true; +} + +async function closePrefsTab() { + info("Closing prefs tab"); + let tabmail = document.getElementById("tabmail"); + let prefsTab = tabmail.tabModes.preferencesTab.tabs[0]; + if (prefsTab) { + tabmail.closeTab(prefsTab); + } +} + +/** + * Tests a checkbox sets the preference is set in the right state when the preferences tab opens, + * that the preference it relates to is set properly, and any UI elements that should be disabled + * by it are disabled. + * + * Each of the tests arguments is an object describing a test, containing: + * checkboxID - the ID of the checkbox to test + * pref - the name of a preference, + * prefValues - an array of two values: pref value when not checked, pref value when checked + * (optional, defaults to [false, true]) + * enabledElements - an array of CSS selectors (optional) + * enabledInverted - if the elements should be disabled when the checkbox is checked (optional) + * unaffectedElements - array of CSS selectors that should not be affected by + * the toggling of the checkbox. + */ +async function testCheckboxes(paneID, scrollPaneTo, ...tests) { + for (let initiallyChecked of [true, false]) { + info(`Opening ${paneID} with prefs set to ${initiallyChecked}`); + + for (let test of tests) { + let wantedValue = initiallyChecked; + if (test.prefValues) { + wantedValue = wantedValue ? test.prefValues[1] : test.prefValues[0]; + } + if (typeof wantedValue == "number") { + Services.prefs.setIntPref(test.pref, wantedValue); + } else { + Services.prefs.setBoolPref(test.pref, wantedValue); + } + } + + let { prefsDocument, prefsWindow } = await openNewPrefsTab( + paneID, + scrollPaneTo + ); + + let testUIState = function (test, checked) { + let wantedValue = checked; + if (test.prefValues) { + wantedValue = wantedValue ? test.prefValues[1] : test.prefValues[0]; + } + let checkbox = prefsDocument.getElementById(test.checkboxID); + is( + checkbox.checked, + checked, + wantedValue, + "Checkbox " + (checked ? "is" : "isn't") + " checked" + ); + if (typeof wantedValue == "number") { + is( + Services.prefs.getIntPref(test.pref, -999), + wantedValue, + `Pref is ${wantedValue}` + ); + } else { + is( + Services.prefs.getBoolPref(test.pref), + wantedValue, + `Pref is ${wantedValue}` + ); + } + + if (test.enabledElements) { + let disabled = checked; + if (test.enabledInverted) { + disabled = !disabled; + } + for (let selector of test.enabledElements) { + let elements = prefsDocument.querySelectorAll(selector); + ok( + elements.length >= 1, + `At least one element matched '${selector}'` + ); + for (let element of elements) { + is( + element.disabled, + !disabled, + "Element " + (disabled ? "isn't" : "is") + " disabled" + ); + } + } + } + }; + + let testUnaffected = function (ids, states) { + ids.forEach((sel, index) => { + let isOk = prefsDocument.querySelector(sel).disabled === states[index]; + is(isOk, true, `Element "${sel}" is unaffected`); + }); + }; + + for (let test of tests) { + info(`Checking ${test.checkboxID}`); + + let unaffectedSelectors = test.unaffectedElements || []; + let unaffectedStates = unaffectedSelectors.map( + sel => prefsDocument.querySelector(sel).disabled + ); + + let checkbox = prefsDocument.getElementById(test.checkboxID); + checkbox.scrollIntoView(false); + testUIState(test, initiallyChecked); + + EventUtils.synthesizeMouseAtCenter(checkbox, {}, prefsWindow); + testUIState(test, !initiallyChecked); + testUnaffected(unaffectedSelectors, unaffectedStates); + + EventUtils.synthesizeMouseAtCenter(checkbox, {}, prefsWindow); + testUIState(test, initiallyChecked); + testUnaffected(unaffectedSelectors, unaffectedStates); + } + + await closePrefsTab(); + } +} + +/** + * Tests a set of radio buttons is in the right state when the preferences tab opens, and when + * the selected button changes that the preference it relates to is set properly, and any related + * UI elements that should be disabled are disabled. + * + * Each of the tests arguments is an object describing a test, containing: + * pref - the name of an integer preference, + * states - an array with each element describing a radio button: + * id - the ID of the button to test, + * prefValue - the value the pref should be set to + * enabledElements - an array of CSS selectors to elements that should be enabled when this + * radio button is selected (optional) + */ +async function testRadioButtons(paneID, scrollPaneTo, ...tests) { + for (let { pref, states } of tests) { + for (let initialState of states) { + info(`Opening ${paneID} with ${pref} set to ${initialState.prefValue}`); + + if (typeof initialState.prefValue == "number") { + Services.prefs.setIntPref(pref, initialState.prefValue); + } else if (typeof initialState.prefValue == "boolean") { + Services.prefs.setBoolPref(pref, initialState.prefValue); + } else { + Services.prefs.setCharPref(pref, initialState.prefValue); + } + + let { prefsDocument, prefsWindow } = await openNewPrefsTab( + paneID, + scrollPaneTo + ); + + let testUIState = function (currentState) { + info(`Testing with ${pref} set to ${currentState.prefValue}`); + for (let state of states) { + let isCurrentState = state == currentState; + let radio = prefsDocument.getElementById(state.id); + is(radio.selected, isCurrentState, `${state.id}.selected`); + + if (state.enabledElements) { + for (let selector of state.enabledElements) { + let elements = prefsDocument.querySelectorAll(selector); + ok( + elements.length >= 1, + `At least one element matched '${selector}'` + ); + for (let element of elements) { + is( + element.disabled, + !isCurrentState, + "Element " + (isCurrentState ? "isn't" : "is") + " disabled" + ); + } + } + } + } + if (typeof initialState.prefValue == "number") { + is( + Services.prefs.getIntPref(pref, -999), + currentState.prefValue, + `Pref is ${currentState.prefValue}` + ); + } else if (typeof initialState.prefValue == "boolean") { + is( + Services.prefs.getBoolPref(pref), + currentState.prefValue, + `Pref is ${currentState.prefValue}` + ); + } else { + is( + Services.prefs.getCharPref(pref, "FAKE VALUE"), + currentState.prefValue, + `Pref is ${currentState.prefValue}` + ); + } + }; + + // Check the initial setup is correct. + testUIState(initialState); + // Cycle through possible values, checking each one. + for (let state of states) { + if (state == initialState) { + continue; + } + let radio = prefsDocument.getElementById(state.id); + radio.scrollIntoView(false); + EventUtils.synthesizeMouseAtCenter(radio, {}, prefsWindow); + testUIState(state); + } + // Go back to the initial value. + let initialRadio = prefsDocument.getElementById(initialState.id); + initialRadio.scrollIntoView(false); + EventUtils.synthesizeMouseAtCenter(initialRadio, {}, prefsWindow); + testUIState(initialState); + + await closePrefsTab(); + } + } +} |