summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/dialogs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/preferences/dialogs')
-rw-r--r--browser/components/preferences/dialogs/addEngine.css24
-rw-r--r--browser/components/preferences/dialogs/addEngine.js69
-rw-r--r--browser/components/preferences/dialogs/addEngine.xhtml72
-rw-r--r--browser/components/preferences/dialogs/applicationManager.js129
-rw-r--r--browser/components/preferences/dialogs/applicationManager.xhtml68
-rw-r--r--browser/components/preferences/dialogs/blocklists.js175
-rw-r--r--browser/components/preferences/dialogs/blocklists.xhtml80
-rw-r--r--browser/components/preferences/dialogs/browserLanguages.js731
-rw-r--r--browser/components/preferences/dialogs/browserLanguages.xhtml84
-rw-r--r--browser/components/preferences/dialogs/clearSiteData.css20
-rw-r--r--browser/components/preferences/dialogs/clearSiteData.js96
-rw-r--r--browser/components/preferences/dialogs/clearSiteData.xhtml70
-rw-r--r--browser/components/preferences/dialogs/colors.js19
-rw-r--r--browser/components/preferences/dialogs/colors.xhtml140
-rw-r--r--browser/components/preferences/dialogs/connection.js381
-rw-r--r--browser/components/preferences/dialogs/connection.xhtml244
-rw-r--r--browser/components/preferences/dialogs/containers.js167
-rw-r--r--browser/components/preferences/dialogs/containers.xhtml72
-rw-r--r--browser/components/preferences/dialogs/dohExceptions.js287
-rw-r--r--browser/components/preferences/dialogs/dohExceptions.xhtml104
-rw-r--r--browser/components/preferences/dialogs/fonts.js173
-rw-r--r--browser/components/preferences/dialogs/fonts.xhtml251
-rw-r--r--browser/components/preferences/dialogs/handlers.css21
-rw-r--r--browser/components/preferences/dialogs/jar.mn49
-rw-r--r--browser/components/preferences/dialogs/languages.js384
-rw-r--r--browser/components/preferences/dialogs/languages.xhtml104
-rw-r--r--browser/components/preferences/dialogs/moz.build13
-rw-r--r--browser/components/preferences/dialogs/permissions.js645
-rw-r--r--browser/components/preferences/dialogs/permissions.xhtml134
-rw-r--r--browser/components/preferences/dialogs/sanitize.js38
-rw-r--r--browser/components/preferences/dialogs/sanitize.xhtml91
-rw-r--r--browser/components/preferences/dialogs/selectBookmark.js119
-rw-r--r--browser/components/preferences/dialogs/selectBookmark.xhtml55
-rw-r--r--browser/components/preferences/dialogs/siteDataRemoveSelected.js56
-rw-r--r--browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml48
-rw-r--r--browser/components/preferences/dialogs/siteDataSettings.js335
-rw-r--r--browser/components/preferences/dialogs/siteDataSettings.xhtml86
-rw-r--r--browser/components/preferences/dialogs/sitePermissions.css70
-rw-r--r--browser/components/preferences/dialogs/sitePermissions.js679
-rw-r--r--browser/components/preferences/dialogs/sitePermissions.xhtml115
-rw-r--r--browser/components/preferences/dialogs/syncChooseWhatToSync.js60
-rw-r--r--browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml88
-rw-r--r--browser/components/preferences/dialogs/translationExceptions.js256
-rw-r--r--browser/components/preferences/dialogs/translationExceptions.xhtml127
-rw-r--r--browser/components/preferences/dialogs/translations.js465
-rw-r--r--browser/components/preferences/dialogs/translations.xhtml158
46 files changed, 7652 insertions, 0 deletions
diff --git a/browser/components/preferences/dialogs/addEngine.css b/browser/components/preferences/dialogs/addEngine.css
new file mode 100644
index 0000000000..450e07f65f
--- /dev/null
+++ b/browser/components/preferences/dialogs/addEngine.css
@@ -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/. */
+
+input {
+ flex: 1;
+}
+
+hbox {
+ width: 100%;
+}
+
+#engineNameLabel,
+#engineUrlLabel,
+#engineAliasLabel {
+ /* Align the labels with the inputs */
+ margin-inline-start: 4px;
+}
+
+#engineUrl {
+ /* Full URLs should always be displayed as LTR */
+ direction: ltr;
+ text-align: match-parent;
+}
diff --git a/browser/components/preferences/dialogs/addEngine.js b/browser/components/preferences/dialogs/addEngine.js
new file mode 100644
index 0000000000..1faf8622b3
--- /dev/null
+++ b/browser/components/preferences/dialogs/addEngine.js
@@ -0,0 +1,69 @@
+/* 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 ../main.js */
+
+let gAddEngineDialog = {
+ _form: null,
+ _name: null,
+ _alias: null,
+
+ onLoad() {
+ document.mozSubdialogReady = this.init();
+ },
+
+ async init() {
+ this._dialog = document.querySelector("dialog");
+ this._form = document.getElementById("addEngineForm");
+ this._name = document.getElementById("engineName");
+ this._alias = document.getElementById("engineAlias");
+
+ this._name.addEventListener("input", this.onNameInput.bind(this));
+ this._alias.addEventListener("input", this.onAliasInput.bind(this));
+ this._form.addEventListener("input", this.onFormInput.bind(this));
+
+ document.addEventListener("dialogaccept", this.onAddEngine.bind(this));
+ },
+
+ async onAddEngine(event) {
+ let url = document
+ .getElementById("engineUrl")
+ .value.replace(/%s/, "{searchTerms}");
+ await Services.search.wrappedJSObject.addUserEngine(
+ this._name.value,
+ url,
+ this._alias.value
+ );
+ },
+
+ async onNameInput() {
+ if (this._name.value) {
+ let engine = Services.search.getEngineByName(this._name.value);
+ let validity = engine
+ ? document.getElementById("engineNameExists").textContent
+ : "";
+ this._name.setCustomValidity(validity);
+ }
+ },
+
+ async onAliasInput() {
+ let validity = "";
+ if (this._alias.value) {
+ let engine = await Services.search.getEngineByAlias(this._alias.value);
+ if (engine) {
+ engine = document.getElementById("engineAliasExists").textContent;
+ }
+ }
+ this._alias.setCustomValidity(validity);
+ },
+
+ async onFormInput() {
+ this._dialog.setAttribute(
+ "buttondisabledaccept",
+ !this._form.checkValidity()
+ );
+ },
+};
+
+window.addEventListener("load", () => gAddEngineDialog.onLoad());
diff --git a/browser/components/preferences/dialogs/addEngine.xhtml b/browser/components/preferences/dialogs/addEngine.xhtml
new file mode 100644
index 0000000000..780cda9010
--- /dev/null
+++ b/browser/components/preferences/dialogs/addEngine.xhtml
@@ -0,0 +1,72 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/addEngine.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="add-engine-window2"
+ data-l10n-attrs="title, style"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ buttondisabledaccept="true"
+ data-l10n-id="add-engine-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/addEngine.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/addEngine.js" />
+ <script src="chrome://global/content/globalOverlay.js" />
+ <script src="chrome://browser/content/utilityOverlay.js" />
+
+ <separator class="thin" />
+
+ <html:form id="addEngineForm">
+ <html:span
+ id="engineNameExists"
+ hidden="hidden"
+ data-l10n-id="engine-name-exists"
+ />
+ <html:label
+ id="engineNameLabel"
+ for="engineName"
+ data-l10n-id="add-engine-name"
+ />
+ <hbox>
+ <html:input id="engineName" type="text" required="required" />
+ </hbox>
+
+ <html:label
+ id="engineUrlLabel"
+ for="engineUrl"
+ data-l10n-id="add-engine-url"
+ />
+ <hbox>
+ <html:input id="engineUrl" type="url" required="required" />
+ </hbox>
+
+ <html:span
+ id="engineAliasExists"
+ hidden="hidden"
+ data-l10n-id="engine-alias-exists"
+ />
+ <html:label
+ id="engineAliasLabel"
+ for="engineAlias"
+ data-l10n-id="add-engine-alias"
+ />
+ <hbox>
+ <html:input id="engineAlias" type="text" />
+ </hbox>
+ </html:form>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/applicationManager.js b/browser/components/preferences/dialogs/applicationManager.js
new file mode 100644
index 0000000000..0c2105cbe4
--- /dev/null
+++ b/browser/components/preferences/dialogs/applicationManager.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 ../main.js */
+
+var gAppManagerDialog = {
+ _removed: [],
+
+ onLoad() {
+ document.mozSubdialogReady = this.init();
+ },
+
+ async init() {
+ this.handlerInfo = window.arguments[0];
+
+ document.addEventListener("dialogaccept", function () {
+ gAppManagerDialog.onOK();
+ });
+
+ let gMainPane = window.parent.gMainPane;
+
+ const appDescElem = document.getElementById("appDescription");
+ if (this.handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
+ let { typeDescription } = this.handlerInfo;
+ let typeStr;
+ if (typeDescription.id) {
+ MozXULElement.insertFTLIfNeeded("browser/preferences/preferences.ftl");
+ typeStr = await document.l10n.formatValue(
+ typeDescription.id,
+ typeDescription.args
+ );
+ } else {
+ typeStr = typeDescription.raw;
+ }
+ document.l10n.setAttributes(appDescElem, "app-manager-handle-file", {
+ type: typeStr,
+ });
+ } else {
+ document.l10n.setAttributes(appDescElem, "app-manager-handle-protocol", {
+ type: this.handlerInfo.typeDescription.raw,
+ });
+ }
+
+ let list = document.getElementById("appList");
+ let listFragment = document.createDocumentFragment();
+ for (let app of this.handlerInfo.possibleApplicationHandlers.enumerate()) {
+ if (!gMainPane.isValidHandlerApp(app)) {
+ continue;
+ }
+
+ let item = document.createXULElement("richlistitem");
+ listFragment.append(item);
+ item.app = app;
+
+ let image = document.createXULElement("image");
+ image.setAttribute("src", gMainPane._getIconURLForHandlerApp(app));
+ item.appendChild(image);
+
+ let label = document.createXULElement("label");
+ label.setAttribute("value", app.name);
+ item.appendChild(label);
+ }
+ list.append(listFragment);
+
+ // Triggers onSelect which populates label
+ list.selectedIndex = 0;
+
+ // We want to block on those elements being localized because the
+ // result will impact the size of the subdialog.
+ await document.l10n.translateElements([
+ appDescElem,
+ document.getElementById("appType"),
+ ]);
+ },
+
+ onOK: function appManager_onOK() {
+ if (this._removed.length) {
+ for (var i = 0; i < this._removed.length; ++i) {
+ this.handlerInfo.removePossibleApplicationHandler(this._removed[i]);
+ }
+
+ this.handlerInfo.store();
+ }
+ },
+
+ remove: function appManager_remove() {
+ var list = document.getElementById("appList");
+ this._removed.push(list.selectedItem.app);
+ var index = list.selectedIndex;
+ var element = list.selectedItem;
+ list.removeItemFromSelection(element);
+ element.remove();
+ if (list.itemCount == 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.itemCount) {
+ --index;
+ }
+ list.selectedIndex = index;
+ }
+ },
+
+ onSelect: function appManager_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;
+ }
+ document.getElementById("appLocation").value = address;
+ const l10nId =
+ app instanceof Ci.nsILocalHandlerApp
+ ? "app-manager-local-app-info"
+ : "app-manager-web-app-info";
+ const appTypeElem = document.getElementById("appType");
+ document.l10n.setAttributes(appTypeElem, l10nId);
+ },
+};
diff --git a/browser/components/preferences/dialogs/applicationManager.xhtml b/browser/components/preferences/dialogs/applicationManager.xhtml
new file mode 100644
index 0000000000..889307d88c
--- /dev/null
+++ b/browser/components/preferences/dialogs/applicationManager.xhtml
@@ -0,0 +1,68 @@
+<?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://browser/skin/preferences/applications.css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gAppManagerDialog.onLoad();"
+ data-l10n-id="app-manager-window2"
+ data-l10n-attrs="title, style"
+>
+ <dialog id="appManager" buttons="accept,cancel">
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/applicationManager.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/dialogs/applicationManager.js" />
+
+ <commandset id="appManagerCommandSet">
+ <command
+ id="cmd_remove"
+ oncommand="gAppManagerDialog.remove();"
+ disabled="true"
+ />
+ </commandset>
+
+ <keyset id="appManagerKeyset">
+ <key id="delete" keycode="VK_DELETE" command="cmd_remove" />
+ </keyset>
+
+ <description id="appDescription" />
+ <separator class="thin" />
+ <hbox flex="1">
+ <richlistbox
+ id="appList"
+ onselect="gAppManagerDialog.onSelect();"
+ flex="1"
+ />
+ <vbox>
+ <button
+ id="remove"
+ data-l10n-id="app-manager-remove"
+ command="cmd_remove"
+ />
+ <spacer flex="1" />
+ </vbox>
+ </hbox>
+ <vbox id="appDetails">
+ <separator class="thin" />
+ <label id="appType" />
+ <html:input
+ type="text"
+ id="appLocation"
+ readonly="readonly"
+ style="margin-inline: 0"
+ />
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/blocklists.js b/browser/components/preferences/dialogs/blocklists.js
new file mode 100644
index 0000000000..c28ee09f96
--- /dev/null
+++ b/browser/components/preferences/dialogs/blocklists.js
@@ -0,0 +1,175 @@
+/* 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 BASE_LIST_ID = "base";
+const CONTENT_LIST_ID = "content";
+const TRACK_SUFFIX = "-track-digest256";
+const TRACKING_TABLE_PREF = "urlclassifier.trackingTable";
+const LISTS_PREF_BRANCH = "browser.safebrowsing.provider.mozilla.lists.";
+
+var gBlocklistManager = {
+ _type: "",
+ _blockLists: [],
+ _tree: null,
+
+ _view: {
+ _rowCount: 0,
+ get rowCount() {
+ return this._rowCount;
+ },
+ getCellText(row, column) {
+ if (column.id == "listCol") {
+ let list = gBlocklistManager._blockLists[row];
+ return list.name;
+ }
+ return "";
+ },
+
+ isSeparator(index) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isContainer(index) {
+ return false;
+ },
+ setTree(tree) {},
+ getImageSrc(row, column) {},
+ getCellValue(row, column) {
+ if (column.id == "selectionCol") {
+ return gBlocklistManager._blockLists[row].selected;
+ }
+ return undefined;
+ },
+ cycleHeader(column) {},
+ getRowProperties(row) {
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ getCellProperties(row, column) {
+ if (column.id == "selectionCol") {
+ return "checkmark";
+ }
+
+ return "";
+ },
+ },
+
+ onLoad() {
+ this.init();
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+ },
+
+ init() {
+ if (this._type) {
+ // reusing an open dialog, clear the old observer
+ this.uninit();
+ }
+
+ this._type = "tracking";
+
+ this._loadBlockLists();
+ },
+
+ uninit() {},
+
+ onListSelected() {
+ for (let list of this._blockLists) {
+ list.selected = false;
+ }
+ this._blockLists[this._tree.currentIndex].selected = true;
+
+ this._updateTree();
+ },
+
+ onApplyChanges() {
+ let activeList = this._getActiveList();
+ let selected = null;
+ for (let list of this._blockLists) {
+ if (list.selected) {
+ selected = list;
+ break;
+ }
+ }
+
+ if (activeList !== selected.id) {
+ let trackingTable = Services.prefs.getCharPref(TRACKING_TABLE_PREF);
+ if (selected.id != CONTENT_LIST_ID) {
+ trackingTable = trackingTable.replace(
+ "," + CONTENT_LIST_ID + TRACK_SUFFIX,
+ ""
+ );
+ } else {
+ trackingTable += "," + CONTENT_LIST_ID + TRACK_SUFFIX;
+ }
+ Services.prefs.setCharPref(TRACKING_TABLE_PREF, trackingTable);
+
+ // Force an update after changing the tracking protection table.
+ let listmanager = Cc[
+ "@mozilla.org/url-classifier/listmanager;1"
+ ].getService(Ci.nsIUrlListManager);
+ if (listmanager) {
+ listmanager.forceUpdates(trackingTable);
+ }
+ }
+ },
+
+ async _loadBlockLists() {
+ this._blockLists = [];
+
+ // Load blocklists into a table.
+ let branch = Services.prefs.getBranch(LISTS_PREF_BRANCH);
+ let itemArray = branch.getChildList("");
+ for (let itemName of itemArray) {
+ try {
+ let list = await this._createBlockList(itemName);
+ this._blockLists.push(list);
+ } catch (e) {
+ // Ignore bogus or missing list name.
+ continue;
+ }
+ }
+
+ this._updateTree();
+ },
+
+ async _createBlockList(id) {
+ let branch = Services.prefs.getBranch(LISTS_PREF_BRANCH);
+ let l10nKey = branch.getCharPref(id);
+
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ let [listName, description] = await document.l10n.formatValues([
+ { id: `blocklist-item-${l10nKey}-listName` },
+ { id: `blocklist-item-${l10nKey}-description` },
+ ]);
+
+ // eslint-disable-next-line mozilla/prefer-formatValues
+ let name = await document.l10n.formatValue("blocklist-item-list-template", {
+ listName,
+ description,
+ });
+
+ return {
+ id,
+ name,
+ selected: this._getActiveList() === id,
+ };
+ },
+
+ _updateTree() {
+ this._tree = document.getElementById("blocklistsTree");
+ this._view._rowCount = this._blockLists.length;
+ this._tree.view = this._view;
+ },
+
+ _getActiveList() {
+ let trackingTable = Services.prefs.getCharPref(TRACKING_TABLE_PREF);
+ return trackingTable.includes(CONTENT_LIST_ID)
+ ? CONTENT_LIST_ID
+ : BASE_LIST_ID;
+ },
+};
diff --git a/browser/components/preferences/dialogs/blocklists.xhtml b/browser/components/preferences/dialogs/blocklists.xhtml
new file mode 100644
index 0000000000..eda42ee7ae
--- /dev/null
+++ b/browser/components/preferences/dialogs/blocklists.xhtml
@@ -0,0 +1,80 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="BlocklistsDialog"
+ data-l10n-id="blocklist-window2"
+ data-l10n-attrs="title, style"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gBlocklistManager.onLoad();"
+ onunload="gBlocklistManager.uninit();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="blocklist-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="browser/preferences/blocklists.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/blocklists.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="blocklist-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <description
+ id="blocklistsText"
+ data-l10n-id="blocklist-description"
+ control="url"
+ >
+ <html:a
+ target="_blank"
+ class="text-link"
+ data-l10n-name="disconnect-link"
+ href="https://disconnect.me/"
+ />
+ </description>
+ <separator class="thin" />
+ <tree
+ id="blocklistsTree"
+ flex="1"
+ style="height: 18em"
+ hidecolumnpicker="true"
+ onselect="gBlocklistManager.onListSelected();"
+ >
+ <treecols>
+ <treecol
+ id="selectionCol"
+ label=""
+ style="flex: 1 auto"
+ sortable="false"
+ type="checkbox"
+ />
+ <treecol
+ id="listCol"
+ data-l10n-id="blocklist-treehead-list"
+ style="flex: 80 80 auto"
+ sortable="false"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/browserLanguages.js b/browser/components/preferences/dialogs/browserLanguages.js
new file mode 100644
index 0000000000..3dc7e3f9ff
--- /dev/null
+++ b/browser/components/preferences/dialogs/browserLanguages.js
@@ -0,0 +1,731 @@
+/* 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 */
+
+// This is exported by preferences.js but we can't import that in a subdialog.
+let { LangPackMatcher } = window.top;
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+});
+ChromeUtils.defineModuleGetter(
+ this,
+ "SelectionChangedMenulist",
+ "resource:///modules/SelectionChangedMenulist.jsm"
+);
+
+document
+ .getElementById("BrowserLanguagesDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+/* This dialog provides an interface for managing what language the browser 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.
+ */
+
+async function installFromUrl(url, hash, callback) {
+ let telemetryInfo = {
+ source: "about:preferences",
+ };
+ let install = await AddonManager.getInstallForURL(url, {
+ hash,
+ telemetryInfo,
+ });
+ if (callback) {
+ callback(install.installId.toString());
+ }
+ await install.install();
+ return install.addon;
+}
+
+async function dictionaryIdsForLocale(locale) {
+ let entries = await RemoteSettings("language-dictionaries").get({
+ filters: { id: locale },
+ });
+ if (entries.length) {
+ return entries[0].dictionaries;
+ }
+ return [];
+}
+
+class OrderedListBox {
+ constructor({
+ richlistbox,
+ upButton,
+ downButton,
+ removeButton,
+ onRemove,
+ onReorder,
+ }) {
+ this.richlistbox = richlistbox;
+ this.upButton = upButton;
+ this.downButton = downButton;
+ this.removeButton = removeButton;
+ this.onRemove = onRemove;
+ this.onReorder = onReorder;
+
+ 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();
+
+ this.onReorder();
+ }
+
+ 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();
+
+ this.onReorder();
+ }
+
+ 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 = [];
+
+ // This will register the "command" listener.
+ new SelectionChangedMenulist(this.menulist, () => {
+ 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://browser/skin/tabbrowser/tab-connecting.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}
+ * @prop {string} id - A unique ID.
+ * @prop {string} label - The localized display name.
+ * @prop {string} value - The BCP 47 locale identifier or the word "search".
+ * @prop {boolean} canRemove - Locales that are part of the packaged locales cannot be
+ * removed.
+ * @prop {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 LangPackMatcher.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 gBrowserLanguagesDialog = {
+ /**
+ * 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 gBrowserLanguagesDialog.
+ *
+ * let { selected } = gBrowserLanguagesDialog;
+ *
+ * @type {null | Array<string>}
+ */
+ selected: null,
+
+ /**
+ * @type {string | null} An ID used for telemetry pings. It is unique to the current
+ * opening of the browser language.
+ */
+ _telemetryId: 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");
+ },
+
+ recordTelemetry(method, extra = null) {
+ Services.telemetry.recordEvent(
+ "intl.ui.browserLanguage",
+ method,
+ "dialog",
+ this._telemetryId,
+ extra
+ );
+ },
+
+ async onLoad() {
+ /**
+ * @typedef {Object} Options - Options passed in to configure the subdialog.
+ * @property {string} telemetryId,
+ * @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 { telemetryId, selectedLocalesForRestart, search } =
+ window.arguments[0];
+
+ this._telemetryId = telemetryId;
+
+ // 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 LangPackMatcher.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("BrowserLanguagesDialog")
+ .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),
+ onReorder: () => this.recordTelemetry("reorder"),
+ });
+ 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") {
+ // Record the search event here so we don't track the search from
+ // the main preferences pane twice.
+ this.recordTelemetry("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(
+ "browser-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 LangPackMatcher.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(
+ "browser-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(
+ "browser-languages-select-language"
+ );
+ },
+
+ /**
+ * @param {Set<string>} available - The set of available (BCP 47) locales.
+ */
+ async loadLocalesFromInstalled(available) {
+ let items;
+ if (available.length) {
+ items = await getLocaleDisplayInfo(available);
+ items.push(await this.createInstalledLabel());
+ } else {
+ items = [];
+ }
+ if (this.downloadEnabled) {
+ items.push({
+ label: await document.l10n.formatValue("browser-languages-search"),
+ value: "search",
+ });
+ }
+ this._availableLocalesUI.setItems(items);
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async availableLanguageSelected(item) {
+ if ((await LangPackMatcher.getAvailableLocales()).includes(item.value)) {
+ this.recordTelemetry("add");
+ await this.requestLocalLanguage(item);
+ } else if (this.availableLangpacks.has(item.value)) {
+ // Telemetry is tracked in requestRemoteLanguage.
+ 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 LangPackMatcher.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(
+ "browser-languages-select-language"
+ );
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async requestRemoteLanguage(item) {
+ this._availableLocalesUI.disableWithMessageId(
+ "browser-languages-downloading"
+ );
+
+ let { url, hash } = this.availableLangpacks.get(item.value);
+ let addon;
+
+ try {
+ addon = await installFromUrl(url, hash, installId =>
+ this.recordTelemetry("add", { installId })
+ );
+ } 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(
+ "browser-languages-select-language"
+ );
+
+ // This is an async task that will install the recommended dictionaries for
+ // this locale. This will fail silently at least until a management UI is
+ // added in bug 1493705.
+ this.installDictionariesForLanguage(item.value);
+ },
+
+ /**
+ * @param {string} locale The BCP 47 locale identifier
+ */
+ async installDictionariesForLanguage(locale) {
+ try {
+ let ids = await dictionaryIdsForLocale(locale);
+ let addonInfos = await AddonRepository.getAddonsByIDs(ids);
+ await Promise.all(
+ addonInfos.map(info => installFromUrl(info.sourceURI.spec))
+ );
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ showError() {
+ document.getElementById("warning-message").hidden = false;
+ this._availableLocalesUI.enableWithMessageId(
+ "browser-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.recordTelemetry("remove");
+
+ 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(
+ "browser-languages-installed-label"
+ ),
+ className: "label-item",
+ disabled: true,
+ installed: true,
+ };
+ },
+};
diff --git a/browser/components/preferences/dialogs/browserLanguages.xhtml b/browser/components/preferences/dialogs/browserLanguages.xhtml
new file mode 100644
index 0000000000..3818ccc058
--- /dev/null
+++ b/browser/components/preferences/dialogs/browserLanguages.xhtml
@@ -0,0 +1,84 @@
+<?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://browser/skin/preferences/preferences.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="browser-languages-window2"
+ data-l10n-attrs="title, style"
+ onload="gBrowserLanguagesDialog.onLoad();"
+>
+ <dialog
+ id="BrowserLanguagesDialog"
+ buttons="accept,cancel,help"
+ helpTopic="change-language"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link rel="localization" href="browser/preferences/languages.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/dialogs/browserLanguages.js" />
+
+ <description data-l10n-id="browser-languages-description" />
+
+ <box class="languages-grid">
+ <richlistbox id="selectedLocales" />
+ <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>
+
+ <menulist
+ id="availableLocales"
+ class="available-locales-list"
+ data-l10n-id="browser-languages-select-language"
+ data-l10n-attrs="placeholder,label"
+ >
+ <menupopup />
+ </menulist>
+ <button
+ id="add"
+ class="add-browser-language action-button"
+ data-l10n-id="languages-customize-add"
+ disabled="true"
+ />
+ </box>
+ <hbox
+ id="warning-message"
+ class="message-bar message-bar-warning"
+ hidden="true"
+ >
+ <image class="message-bar-icon" />
+ <description
+ class="message-bar-description"
+ data-l10n-id="browser-languages-error"
+ />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/clearSiteData.css b/browser/components/preferences/dialogs/clearSiteData.css
new file mode 100644
index 0000000000..5b1f5bbe2d
--- /dev/null
+++ b/browser/components/preferences/dialogs/clearSiteData.css
@@ -0,0 +1,20 @@
+/* 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/. */
+
+.options-container {
+ background-color: var(--in-content-box-background);
+ border: 1px solid var(--in-content-box-border-color);
+ border-radius: 2px;
+ color: var(--in-content-text-color);
+ padding: 0.5em;
+}
+
+.option {
+ padding-bottom: 8px;
+}
+
+.option-description {
+ color: var(--text-color-deemphasized);
+ margin-top: -0.5em !important;
+}
diff --git a/browser/components/preferences/dialogs/clearSiteData.js b/browser/components/preferences/dialogs/clearSiteData.js
new file mode 100644
index 0000000000..eae2d5f772
--- /dev/null
+++ b/browser/components/preferences/dialogs/clearSiteData.js
@@ -0,0 +1,96 @@
+/* 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 { SiteDataManager } = ChromeUtils.import(
+ "resource:///modules/SiteDataManager.jsm"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+});
+
+var gClearSiteDataDialog = {
+ _clearSiteDataCheckbox: null,
+ _clearCacheCheckbox: null,
+
+ onLoad() {
+ document.mozSubdialogReady = this.init();
+ },
+
+ async init() {
+ this._dialog = document.querySelector("dialog");
+ this._clearSiteDataCheckbox = document.getElementById("clearSiteData");
+ this._clearCacheCheckbox = document.getElementById("clearCache");
+
+ // We'll block init() on this because the result values may impact
+ // subdialog sizing.
+ await Promise.all([
+ SiteDataManager.getTotalUsage().then(bytes => {
+ let [amount, unit] = DownloadUtils.convertByteUnits(bytes);
+ document.l10n.setAttributes(
+ this._clearSiteDataCheckbox,
+ "clear-site-data-cookies-with-data",
+ { amount, unit }
+ );
+ }),
+ SiteDataManager.getCacheSize().then(bytes => {
+ let [amount, unit] = DownloadUtils.convertByteUnits(bytes);
+ document.l10n.setAttributes(
+ this._clearCacheCheckbox,
+ "clear-site-data-cache-with-data",
+ { amount, unit }
+ );
+ }),
+ ]);
+ await document.l10n.translateElements([
+ this._clearCacheCheckbox,
+ this._clearSiteDataCheckbox,
+ ]);
+
+ document.addEventListener("dialogaccept", event => this.onClear(event));
+
+ this._clearSiteDataCheckbox.addEventListener("command", e =>
+ this.onCheckboxCommand(e)
+ );
+ this._clearCacheCheckbox.addEventListener("command", e =>
+ this.onCheckboxCommand(e)
+ );
+ },
+
+ onCheckboxCommand(event) {
+ this._dialog.setAttribute(
+ "buttondisabledaccept",
+ !(this._clearSiteDataCheckbox.checked || this._clearCacheCheckbox.checked)
+ );
+ },
+
+ onClear(event) {
+ let clearSiteData = this._clearSiteDataCheckbox.checked;
+ let clearCache = this._clearCacheCheckbox.checked;
+
+ if (clearSiteData) {
+ // Ask for confirmation before clearing site data
+ if (!SiteDataManager.promptSiteDataRemoval(window)) {
+ clearSiteData = false;
+ // Prevent closing the dialog when the data removal wasn't allowed.
+ event.preventDefault();
+ }
+ }
+
+ if (clearSiteData) {
+ SiteDataManager.removeSiteData();
+ }
+ if (clearCache) {
+ SiteDataManager.removeCache();
+
+ // If we're not clearing site data, we need to tell the
+ // SiteDataManager to signal that it's updating.
+ if (!clearSiteData) {
+ SiteDataManager.updateSites();
+ }
+ }
+ },
+};
+
+window.addEventListener("load", () => gClearSiteDataDialog.onLoad());
diff --git a/browser/components/preferences/dialogs/clearSiteData.xhtml b/browser/components/preferences/dialogs/clearSiteData.xhtml
new file mode 100644
index 0000000000..574dc4fb5d
--- /dev/null
+++ b/browser/components/preferences/dialogs/clearSiteData.xhtml
@@ -0,0 +1,70 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/clearSiteData.css" type="text/css"?>
+
+<window
+ id="ClearSiteDataDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="clear-site-data-window2"
+ data-l10n-attrs="title, style"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="clear-site-data-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/clearSiteData.ftl"
+ />
+ </linkset>
+ <script src="chrome://browser/content/preferences/dialogs/clearSiteData.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="clear-site-data-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <description control="url" data-l10n-id="clear-site-data-description" />
+ <separator class="thin" />
+ <vbox class="options-container">
+ <vbox class="option">
+ <checkbox
+ data-l10n-id="clear-site-data-cookies-empty"
+ id="clearSiteData"
+ checked="true"
+ />
+ <description
+ class="option-description indent"
+ data-l10n-id="clear-site-data-cookies-info"
+ />
+ </vbox>
+ <vbox class="option">
+ <checkbox
+ data-l10n-id="clear-site-data-cache-empty"
+ id="clearCache"
+ checked="true"
+ />
+ <description
+ class="option-description indent"
+ data-l10n-id="clear-site-data-cache-info"
+ />
+ </vbox>
+ </vbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/colors.js b/browser/components/preferences/dialogs/colors.js
new file mode 100644
index 0000000000..3bb78e5ec1
--- /dev/null
+++ b/browser/components/preferences/dialogs/colors.js
@@ -0,0 +1,19 @@
+/* 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 */
+
+document
+ .getElementById("ColorsDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+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/browser/components/preferences/dialogs/colors.xhtml b/browser/components/preferences/dialogs/colors.xhtml
new file mode 100644
index 0000000000..8d81273880
--- /dev/null
+++ b/browser/components/preferences/dialogs/colors.xhtml
@@ -0,0 +1,140 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.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="colors-dialog2"
+ data-l10n-attrs="title, style"
+ persist="lastSelected"
+>
+ <dialog
+ id="ColorsDialog"
+ buttons="accept,cancel,help"
+ helpTopic="prefs-fonts-and-colors"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/colors.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="colors-close-key"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <hbox>
+ <groupbox flex="1">
+ <label><html:h2 data-l10n-id="colors-text-and-background" /></label>
+ <hbox align="center">
+ <label
+ data-l10n-id="colors-text-header"
+ 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="colors-background" 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="colors-use-system"
+ preference="browser.display.use_system_colors"
+ />
+ </hbox>
+ </groupbox>
+
+ <groupbox flex="1">
+ <label><html:h2 data-l10n-id="colors-links-header" /></label>
+ <hbox align="center">
+ <label
+ data-l10n-id="colors-unvisited-links"
+ 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="colors-visited-links"
+ 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="colors-underline-links"
+ preference="browser.underline_anchors"
+ />
+ </hbox>
+ </groupbox>
+ </hbox>
+
+ <label data-l10n-id="colors-page-override" control="useDocumentColors" />
+ <hbox>
+ <menulist
+ id="useDocumentColors"
+ preference="browser.display.document_color_use"
+ flex="1"
+ >
+ <menupopup>
+ <menuitem
+ data-l10n-id="colors-page-override-option-always"
+ value="2"
+ id="documentColorAlways"
+ />
+ <menuitem
+ data-l10n-id="colors-page-override-option-auto"
+ value="0"
+ id="documentColorAutomatic"
+ />
+ <menuitem
+ data-l10n-id="colors-page-override-option-never"
+ value="1"
+ id="documentColorNever"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <!-- Load the script after the elements for layout issues (bug 1501755). -->
+ <script src="chrome://browser/content/preferences/dialogs/colors.js" />
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/connection.js b/browser/components/preferences/dialogs/connection.js
new file mode 100644
index 0000000000..ee669b3762
--- /dev/null
+++ b/browser/components/preferences/dialogs/connection.js
@@ -0,0 +1,381 @@
+/* -*- 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 /browser/base/content/utilityOverlay.js */
+/* import-globals-from /toolkit/content/preferencesBindings.js */
+/* import-globals-from ../extensionControlled.js */
+
+document
+ .getElementById("ConnectionsDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+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" },
+]);
+
+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)
+ );
+
+ document
+ .getElementById("disableProxyExtension")
+ .addEventListener(
+ "command",
+ makeDisableControllingExtension(PREF_SETTING_TYPE, PROXY_KEY).bind(
+ gConnectionsDialog
+ )
+ );
+ gConnectionsDialog.updateProxySettingsUI();
+ initializeProxyUI(gConnectionsDialog);
+ gConnectionsDialog.registerSyncPrefListeners();
+ document
+ .getElementById("ConnectionsDialog")
+ .addEventListener("beforeaccept", e =>
+ gConnectionsDialog.beforeAccept(e)
+ );
+ },
+ { once: true, capture: true }
+);
+
+var gConnectionsDialog = {
+ beforeAccept(event) {
+ 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 input 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
+ ).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.
+ hideControllingExtension(PROXY_KEY);
+ } else {
+ handleControllingExtension(PREF_SETTING_TYPE, PROXY_KEY).then(
+ setInputsDisabledState
+ );
+ }
+ },
+
+ registerSyncPrefListeners() {
+ function setSyncFromPrefListener(element_id, callback) {
+ Preferences.addSyncFromPrefListener(
+ document.getElementById(element_id),
+ callback
+ );
+ }
+ setSyncFromPrefListener("networkProxyType", () => this.readProxyType());
+ setSyncFromPrefListener("networkProxyHTTP", () =>
+ this.readHTTPProxyServer()
+ );
+ setSyncFromPrefListener("networkProxyHTTP_Port", () =>
+ this.readHTTPProxyPort()
+ );
+ setSyncFromPrefListener("shareAllProxies", () =>
+ this.updateProtocolPrefs()
+ );
+ setSyncFromPrefListener("networkProxySSL", () =>
+ this.readProxyProtocolPref("ssl", false)
+ );
+ setSyncFromPrefListener("networkProxySSL_Port", () =>
+ this.readProxyProtocolPref("ssl", true)
+ );
+ setSyncFromPrefListener("networkProxySOCKS", () =>
+ this.readProxyProtocolPref("socks", false)
+ );
+ setSyncFromPrefListener("networkProxySOCKS_Port", () =>
+ this.readProxyProtocolPref("socks", true)
+ );
+ },
+};
diff --git a/browser/components/preferences/dialogs/connection.xhtml b/browser/components/preferences/dialogs/connection.xhtml
new file mode 100644
index 0000000000..4f161a710d
--- /dev/null
+++ b/browser/components/preferences/dialogs/connection.xhtml
@@ -0,0 +1,244 @@
+<?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://browser/skin/preferences/preferences.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-window2"
+ data-l10n-attrs="title, style"
+ persist="lastSelected"
+ onload="gConnectionsDialog.checkForSystemProxy();"
+>
+ <dialog
+ id="ConnectionsDialog"
+ buttons="accept,cancel,help"
+ helpTopic="prefs-connection-settings"
+ >
+ <!-- Used for extension-controlled lockdown message -->
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/connection.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/preferences.ftl"
+ />
+ <html:link rel="localization" href="branding/brand.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/extensionControlled.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="connection-close-key"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <script src="chrome://browser/content/preferences/dialogs/connection.js" />
+
+ <hbox
+ id="proxyExtensionContent"
+ align="start"
+ hidden="true"
+ class="extension-controlled"
+ >
+ <description control="disableProxyExtension" flex="1" />
+ <button
+ id="disableProxyExtension"
+ class="extension-controlled-button accessory-button"
+ data-l10n-id="connection-disable-extension"
+ />
+ </hbox>
+
+ <groupbox>
+ <label><html:h2 data-l10n-id="connection-proxy-configure" /></label>
+
+ <radiogroup id="networkProxyType" preference="network.proxy.type">
+ <radio value="0" data-l10n-id="connection-proxy-option-no" />
+ <radio value="4" data-l10n-id="connection-proxy-option-auto" />
+ <radio
+ value="5"
+ data-l10n-id="connection-proxy-option-system"
+ id="systemPref"
+ hidden="true"
+ />
+ <radio value="1" data-l10n-id="connection-proxy-option-manual" />
+ <box id="proxy-grid" class="indent" flex="1">
+ <html:div class="proxy-grid-row">
+ <hbox pack="end">
+ <label
+ data-l10n-id="connection-proxy-http"
+ control="networkProxyHTTP"
+ />
+ </hbox>
+ <hbox align="center">
+ <html:input
+ id="networkProxyHTTP"
+ type="text"
+ style="flex: 1"
+ preference="network.proxy.http"
+ />
+ <label
+ data-l10n-id="connection-proxy-http-port"
+ control="networkProxyHTTP_Port"
+ />
+ <html:input
+ id="networkProxyHTTP_Port"
+ class="proxy-port-input"
+ hidespinbuttons="true"
+ type="number"
+ min="0"
+ max="65535"
+ preference="network.proxy.http_port"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="proxy-grid-row">
+ <hbox />
+ <hbox>
+ <checkbox
+ id="shareAllProxies"
+ data-l10n-id="connection-proxy-https-sharing"
+ preference="network.proxy.share_proxy_settings"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="proxy-grid-row">
+ <hbox pack="end">
+ <label
+ data-l10n-id="connection-proxy-https"
+ control="networkProxySSL"
+ />
+ </hbox>
+ <hbox align="center">
+ <html:input
+ id="networkProxySSL"
+ type="text"
+ style="flex: 1"
+ preference="network.proxy.ssl"
+ />
+ <label
+ data-l10n-id="connection-proxy-ssl-port"
+ control="networkProxySSL_Port"
+ />
+ <html:input
+ id="networkProxySSL_Port"
+ class="proxy-port-input"
+ hidespinbuttons="true"
+ type="number"
+ min="0"
+ max="65535"
+ size="5"
+ preference="network.proxy.ssl_port"
+ />
+ </hbox>
+ </html:div>
+ <separator class="thin" />
+ <html:div class="proxy-grid-row">
+ <hbox pack="end">
+ <label
+ data-l10n-id="connection-proxy-socks"
+ control="networkProxySOCKS"
+ />
+ </hbox>
+ <hbox align="center">
+ <html:input
+ id="networkProxySOCKS"
+ type="text"
+ style="flex: 1"
+ preference="network.proxy.socks"
+ />
+ <label
+ data-l10n-id="connection-proxy-socks-port"
+ control="networkProxySOCKS_Port"
+ />
+ <html:input
+ id="networkProxySOCKS_Port"
+ class="proxy-port-input"
+ hidespinbuttons="true"
+ type="number"
+ min="0"
+ max="65535"
+ size="5"
+ preference="network.proxy.socks_port"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="proxy-grid-row">
+ <spacer />
+ <box pack="start">
+ <radiogroup
+ id="networkProxySOCKSVersion"
+ orient="horizontal"
+ preference="network.proxy.socks_version"
+ >
+ <radio
+ id="networkProxySOCKSVersion4"
+ value="4"
+ data-l10n-id="connection-proxy-socks4"
+ />
+ <radio
+ id="networkProxySOCKSVersion5"
+ value="5"
+ data-l10n-id="connection-proxy-socks5"
+ />
+ </radiogroup>
+ </box>
+ </html:div>
+ </box>
+ <radio value="2" data-l10n-id="connection-proxy-autotype" />
+ <hbox class="indent" flex="1" align="center">
+ <html:input
+ id="networkProxyAutoconfigURL"
+ type="text"
+ style="flex: 1"
+ preference="network.proxy.autoconfig_url"
+ oninput="gConnectionsDialog.updateReloadButton();"
+ />
+ <button
+ id="autoReload"
+ data-l10n-id="connection-proxy-reload"
+ oncommand="gConnectionsDialog.reloadPAC();"
+ preference="pref.advanced.proxies.disable_button.reload"
+ />
+ </hbox>
+ </radiogroup>
+ </groupbox>
+ <separator class="thin" />
+ <label data-l10n-id="connection-proxy-noproxy" control="networkProxyNone" />
+ <html:textarea
+ id="networkProxyNone"
+ preference="network.proxy.no_proxies_on"
+ rows="2"
+ />
+ <label
+ control="networkProxyNone"
+ data-l10n-id="connection-proxy-noproxy-desc"
+ />
+ <label
+ id="networkProxyNoneLocalhost"
+ control="networkProxyNone"
+ data-l10n-id="connection-proxy-noproxy-localhost-desc-2"
+ />
+ <separator class="thin" />
+ <checkbox
+ id="autologinProxy"
+ data-l10n-id="connection-proxy-autologin"
+ preference="signon.autologin.proxy"
+ />
+ <checkbox
+ id="networkProxySOCKSRemoteDNS"
+ preference="network.proxy.socks_remote_dns"
+ data-l10n-id="connection-proxy-socks-remote-dns"
+ />
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/containers.js b/browser/components/preferences/dialogs/containers.js
new file mode 100644
index 0000000000..14526545b6
--- /dev/null
+++ b/browser/components/preferences/dialogs/containers.js
@@ -0,0 +1,167 @@
+/* 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 { ContextualIdentityService } = ChromeUtils.importESModule(
+ "resource://gre/modules/ContextualIdentityService.sys.mjs"
+);
+
+/**
+ * We want to set the window title immediately to prevent flickers.
+ */
+function setTitle() {
+ let params = window.arguments[0] || {};
+
+ let winElem = document.documentElement;
+ if (params.userContextId) {
+ document.l10n.setAttributes(winElem, "containers-window-update-settings2", {
+ name: params.identity.name,
+ });
+ } else {
+ document.l10n.setAttributes(winElem, "containers-window-new2");
+ }
+}
+setTitle();
+
+let gContainersManager = {
+ icons: [
+ "fingerprint",
+ "briefcase",
+ "dollar",
+ "cart",
+ "vacation",
+ "gift",
+ "food",
+ "fruit",
+ "pet",
+ "tree",
+ "chill",
+ "circle",
+ "fence",
+ ],
+
+ colors: [
+ "blue",
+ "turquoise",
+ "green",
+ "yellow",
+ "orange",
+ "red",
+ "pink",
+ "purple",
+ "toolbar",
+ ],
+
+ onLoad() {
+ let params = window.arguments[0] || {};
+ this.init(params);
+ },
+
+ init(aParams) {
+ this._dialog = document.querySelector("dialog");
+ this.userContextId = aParams.userContextId || null;
+ this.identity = aParams.identity;
+
+ const iconWrapper = document.getElementById("iconWrapper");
+ iconWrapper.appendChild(this.createIconButtons());
+
+ const colorWrapper = document.getElementById("colorWrapper");
+ colorWrapper.appendChild(this.createColorSwatches());
+
+ if (this.identity.name) {
+ const name = document.getElementById("name");
+ name.value = this.identity.name;
+ this.checkForm();
+ }
+
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+
+ // This is to prevent layout jank caused by the svgs and outlines rendering at different times
+ document.getElementById("containers-content").removeAttribute("hidden");
+ },
+
+ uninit() {},
+
+ // Check if name is provided to determine if the form can be submitted
+ checkForm() {
+ const name = document.getElementById("name");
+ this._dialog.setAttribute("buttondisabledaccept", !name.value.trim());
+ },
+
+ createIconButtons(defaultIcon) {
+ let radiogroup = document.createXULElement("radiogroup");
+ radiogroup.setAttribute("id", "icon");
+ radiogroup.className = "icon-buttons radio-buttons";
+
+ for (let icon of this.icons) {
+ let iconSwatch = document.createXULElement("radio");
+ iconSwatch.id = "iconbutton-" + icon;
+ iconSwatch.name = "icon";
+ iconSwatch.type = "radio";
+ iconSwatch.value = icon;
+
+ if (this.identity.icon && this.identity.icon == icon) {
+ iconSwatch.setAttribute("selected", true);
+ }
+
+ document.l10n.setAttributes(iconSwatch, `containers-icon-${icon}`);
+ let iconElement = document.createXULElement("hbox");
+ iconElement.className = "userContext-icon";
+ iconElement.classList.add("identity-icon-" + icon);
+
+ iconSwatch.appendChild(iconElement);
+ radiogroup.appendChild(iconSwatch);
+ }
+
+ return radiogroup;
+ },
+
+ createColorSwatches(defaultColor) {
+ let radiogroup = document.createXULElement("radiogroup");
+ radiogroup.setAttribute("id", "color");
+ radiogroup.className = "radio-buttons";
+
+ for (let color of this.colors) {
+ let colorSwatch = document.createXULElement("radio");
+ colorSwatch.id = "colorswatch-" + color;
+ colorSwatch.name = "color";
+ colorSwatch.type = "radio";
+ colorSwatch.value = color;
+
+ if (this.identity.color && this.identity.color == color) {
+ colorSwatch.setAttribute("selected", true);
+ }
+
+ document.l10n.setAttributes(colorSwatch, `containers-color-${color}`);
+ let iconElement = document.createXULElement("hbox");
+ iconElement.className = "userContext-icon";
+ iconElement.classList.add("identity-icon-circle");
+ iconElement.classList.add("identity-color-" + color);
+
+ colorSwatch.appendChild(iconElement);
+ radiogroup.appendChild(colorSwatch);
+ }
+ return radiogroup;
+ },
+
+ onApplyChanges() {
+ let icon = document.getElementById("icon").value;
+ let color = document.getElementById("color").value;
+ let name = document.getElementById("name").value;
+
+ if (!this.icons.includes(icon)) {
+ throw new Error("Internal error. The icon value doesn't match.");
+ }
+
+ if (!this.colors.includes(color)) {
+ throw new Error("Internal error. The color value doesn't match.");
+ }
+
+ if (this.userContextId) {
+ ContextualIdentityService.update(this.userContextId, name, icon, color);
+ } else {
+ ContextualIdentityService.create(name, icon, color);
+ }
+ window.parent.location.reload();
+ },
+};
diff --git a/browser/components/preferences/dialogs/containers.xhtml b/browser/components/preferences/dialogs/containers.xhtml
new file mode 100644
index 0000000000..446a78aee0
--- /dev/null
+++ b/browser/components/preferences/dialogs/containers.xhtml
@@ -0,0 +1,72 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/containers-dialog.css" type="text/css"?>
+
+<window
+ id="ContainersDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-attrs="title, style"
+ onload="gContainersManager.onLoad();"
+ onunload="gContainersManager.uninit();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept"
+ buttondisabledaccept="true"
+ data-l10n-id="containers-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/containers.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/containers.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="containers-window-close"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane" hidden="true" id="containers-content">
+ <hbox align="start">
+ <label
+ id="nameLabel"
+ control="name"
+ data-l10n-id="containers-name-label"
+ data-l10n-attrs="style"
+ />
+ <html:input
+ id="name"
+ type="text"
+ data-l10n-id="containers-name-text"
+ oninput="gContainersManager.checkForm();"
+ />
+ </hbox>
+ <hbox align="center" id="colorWrapper">
+ <label
+ id="colorLabel"
+ control="color"
+ data-l10n-id="containers-color-label"
+ data-l10n-attrs="style"
+ />
+ </hbox>
+ <hbox align="center" id="iconWrapper">
+ <label
+ id="iconLabel"
+ control="icon"
+ data-l10n-id="containers-icon-label"
+ data-l10n-attrs="style"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/dohExceptions.js b/browser/components/preferences/dialogs/dohExceptions.js
new file mode 100644
index 0000000000..f4c1d4d80d
--- /dev/null
+++ b/browser/components/preferences/dialogs/dohExceptions.js
@@ -0,0 +1,287 @@
+/* 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 { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var gDoHExceptionsManager = {
+ _exceptions: new Set(),
+ _list: null,
+ _prefLocked: false,
+
+ init() {
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+
+ this._btnAddException = document.getElementById("btnAddException");
+ this._removeButton = document.getElementById("removeException");
+ this._removeAllButton = document.getElementById("removeAllExceptions");
+ this._list = document.getElementById("permissionsBox");
+
+ this._urlField = document.getElementById("url");
+ this.onExceptionInput();
+
+ this._loadExceptions();
+ this.buildExceptionList();
+
+ this._urlField.focus();
+
+ this._prefLocked = Services.prefs.prefIsLocked(
+ "network.trr.excluded-domains"
+ );
+
+ this._btnAddException.disabled = this._prefLocked;
+ document.getElementById("exceptionDialog").getButton("accept").disabled =
+ this._prefLocked;
+ this._urlField.disabled = this._prefLocked;
+ },
+
+ _loadExceptions() {
+ let exceptionsFromPref = Services.prefs.getStringPref(
+ "network.trr.excluded-domains"
+ );
+
+ if (!exceptionsFromPref?.trim()) {
+ return;
+ }
+
+ let exceptions = exceptionsFromPref.trim().split(",");
+ for (let exception of exceptions) {
+ let trimmed = exception.trim();
+ if (trimmed) {
+ this._exceptions.add(trimmed);
+ }
+ }
+ },
+
+ addException() {
+ if (this._prefLocked) {
+ return;
+ }
+
+ let textbox = document.getElementById("url");
+ let inputValue = textbox.value.trim(); // trim any leading and trailing space
+ if (!inputValue.startsWith("http:") && !inputValue.startsWith("https:")) {
+ inputValue = `http://${inputValue}`;
+ }
+ let domain = "";
+ try {
+ let uri = Services.io.newURI(inputValue);
+ domain = uri.host;
+ } catch (ex) {
+ document.l10n
+ .formatValues([
+ { id: "permissions-invalid-uri-title" },
+ { id: "permissions-invalid-uri-label" },
+ ])
+ .then(([title, message]) => {
+ Services.prompt.alert(window, title, message);
+ });
+ return;
+ }
+
+ if (!this._exceptions.has(domain)) {
+ this._exceptions.add(domain);
+ this.buildExceptionList();
+ }
+
+ textbox.value = "";
+ textbox.focus();
+
+ // covers a case where the site exists already, so the buttons don't disable
+ this.onExceptionInput();
+
+ // enable "remove all" button as needed
+ this._setRemoveButtonState();
+ },
+
+ onExceptionInput() {
+ this._btnAddException.disabled = !this._urlField.value;
+ },
+
+ onExceptionKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
+ this._btnAddException.click();
+ if (document.activeElement == this._urlField) {
+ event.preventDefault();
+ }
+ }
+ },
+
+ onListBoxKeyPress(event) {
+ if (!this._list.selectedItem) {
+ return;
+ }
+
+ if (this._prefLocked) {
+ return;
+ }
+
+ if (
+ event.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+ ) {
+ this.onExceptionDelete();
+ event.preventDefault();
+ }
+ },
+
+ onListBoxSelect() {
+ this._setRemoveButtonState();
+ },
+
+ _removeExceptionFromList(exception) {
+ this._exceptions.delete(exception);
+ let exceptionlistitem = document.getElementsByAttribute(
+ "domain",
+ exception
+ )[0];
+ if (exceptionlistitem) {
+ exceptionlistitem.remove();
+ }
+ },
+
+ onExceptionDelete() {
+ let richlistitem = this._list.selectedItem;
+ let exception = richlistitem.getAttribute("domain");
+
+ this._removeExceptionFromList(exception);
+
+ this._setRemoveButtonState();
+ },
+
+ onAllExceptionsDelete() {
+ for (let exception of this._exceptions.values()) {
+ this._removeExceptionFromList(exception);
+ }
+
+ this._setRemoveButtonState();
+ },
+
+ _createExceptionListItem(exception) {
+ let richlistitem = document.createXULElement("richlistitem");
+ richlistitem.setAttribute("domain", exception);
+ let row = document.createXULElement("hbox");
+ row.setAttribute("style", "flex: 1");
+
+ let hbox = document.createXULElement("hbox");
+ let website = document.createXULElement("label");
+ website.setAttribute("class", "website-name-value");
+ website.setAttribute("value", exception);
+ hbox.setAttribute("class", "website-name");
+ hbox.setAttribute("style", "flex: 3 3; width: 0");
+ hbox.appendChild(website);
+ row.appendChild(hbox);
+
+ richlistitem.appendChild(row);
+ return richlistitem;
+ },
+
+ _sortExceptions(list, frag, column) {
+ let sortDirection;
+
+ if (!column) {
+ column = document.querySelector("treecol[data-isCurrentSortCol=true]");
+ sortDirection =
+ column.getAttribute("data-last-sortDirection") || "ascending";
+ } else {
+ sortDirection = column.getAttribute("data-last-sortDirection");
+ sortDirection =
+ sortDirection === "ascending" ? "descending" : "ascending";
+ }
+
+ let sortFunc = (a, b) => {
+ return comp.compare(a.getAttribute("domain"), b.getAttribute("domain"));
+ };
+
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ let items = Array.from(frag.querySelectorAll("richlistitem"));
+
+ if (sortDirection === "descending") {
+ items.sort((a, b) => sortFunc(b, a));
+ } else {
+ items.sort(sortFunc);
+ }
+
+ // Re-append items in the correct order:
+ items.forEach(item => frag.appendChild(item));
+
+ let cols = list.previousElementSibling.querySelectorAll("treecol");
+ cols.forEach(c => {
+ c.removeAttribute("data-isCurrentSortCol");
+ c.removeAttribute("sortDirection");
+ });
+ column.setAttribute("data-isCurrentSortCol", "true");
+ column.setAttribute("sortDirection", sortDirection);
+ column.setAttribute("data-last-sortDirection", sortDirection);
+ },
+
+ _setRemoveButtonState() {
+ if (!this._list) {
+ return;
+ }
+
+ if (this._prefLocked) {
+ this._removeAllButton.disabled = true;
+ this._removeButton.disabled = true;
+ return;
+ }
+
+ let hasSelection = this._list.selectedIndex >= 0;
+
+ this._removeButton.disabled = !hasSelection;
+ let disabledItems = this._list.querySelectorAll(
+ "label.website-name-value[disabled='true']"
+ );
+
+ this._removeAllButton.disabled =
+ this._list.itemCount == disabledItems.length;
+ },
+
+ onApplyChanges() {
+ if (this._exceptions.size == 0) {
+ Services.prefs.setStringPref("network.trr.excluded-domains", "");
+ return;
+ }
+
+ let exceptions = Array.from(this._exceptions);
+ let exceptionPrefString = exceptions.join(",");
+
+ Services.prefs.setStringPref(
+ "network.trr.excluded-domains",
+ exceptionPrefString
+ );
+ },
+
+ buildExceptionList(sortCol) {
+ // Clear old entries.
+ let oldItems = this._list.querySelectorAll("richlistitem");
+ for (let item of oldItems) {
+ item.remove();
+ }
+ let frag = document.createDocumentFragment();
+
+ let exceptions = Array.from(this._exceptions.values());
+
+ for (let exception of exceptions) {
+ let richlistitem = this._createExceptionListItem(exception);
+ frag.appendChild(richlistitem);
+ }
+
+ // Sort exceptions.
+ this._sortExceptions(this._list, frag, sortCol);
+
+ this._list.appendChild(frag);
+
+ this._setRemoveButtonState();
+ },
+};
+
+document.addEventListener("DOMContentLoaded", () => {
+ gDoHExceptionsManager.init();
+});
diff --git a/browser/components/preferences/dialogs/dohExceptions.xhtml b/browser/components/preferences/dialogs/dohExceptions.xhtml
new file mode 100644
index 0000000000..617e9fbfb0
--- /dev/null
+++ b/browser/components/preferences/dialogs/dohExceptions.xhtml
@@ -0,0 +1,104 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/sitePermissions.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="DoHExceptionsDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="permissions-exceptions-doh-window"
+ data-l10n-attrs="title, style"
+ persist="width height"
+>
+ <dialog
+ id="exceptionDialog"
+ buttons="accept,cancel"
+ data-l10n-id="permission-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/permissions.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/dohExceptions.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="permissions-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <description
+ id="dohExceptionText"
+ control="url"
+ data-l10n-id="permissions-exceptions-manage-doh-desc"
+ />
+ <separator class="thin" />
+ <label
+ id="urlLabel"
+ control="url"
+ data-l10n-id="permissions-doh-entry-field"
+ />
+ <hbox align="start">
+ <html:input
+ id="url"
+ type="text"
+ style="flex: 1"
+ oninput="gDoHExceptionsManager.onExceptionInput();"
+ onkeypress="gDoHExceptionsManager.onExceptionKeyPress(event);"
+ />
+ </hbox>
+ <hbox pack="end">
+ <button
+ id="btnAddException"
+ disabled="true"
+ data-l10n-id="permissions-doh-add-exception"
+ oncommand="gDoHExceptionsManager.addException();"
+ />
+ </hbox>
+ <separator class="thin" />
+ <listheader>
+ <treecol
+ id="siteCol"
+ data-l10n-id="permissions-doh-col"
+ style="flex: 3 3 auto; width: 0"
+ data-isCurrentSortCol="true"
+ onclick="gDoHExceptionsManager.buildExceptionList(event.target)"
+ />
+ </listheader>
+ <richlistbox
+ id="permissionsBox"
+ selected="false"
+ onkeypress="gDoHExceptionsManager.onListBoxKeyPress(event);"
+ onselect="gDoHExceptionsManager.onListBoxSelect();"
+ />
+ </vbox>
+
+ <hbox class="actionButtons">
+ <button
+ id="removeException"
+ disabled="true"
+ data-l10n-id="permissions-doh-remove"
+ oncommand="gDoHExceptionsManager.onExceptionDelete();"
+ />
+ <button
+ id="removeAllExceptions"
+ data-l10n-id="permissions-doh-remove-all"
+ oncommand="gDoHExceptionsManager.onAllExceptionsDelete();"
+ />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/fonts.js b/browser/components/preferences/dialogs/fonts.js
new file mode 100644
index 0000000000..ccc2f8faca
--- /dev/null
+++ b/browser/components/preferences/dialogs/fonts.js
@@ -0,0 +1,173 @@
+/* -*- 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 /browser/base/content/utilityOverlay.js */
+/* import-globals-from /toolkit/mozapps/preferences/fontbuilder.js */
+
+// browser.display.languageList LOCK ALL when LOCKED
+
+const kDefaultFontType = "font.default.%LANG%";
+const kFontNameFmtSerif = "font.name.serif.%LANG%";
+const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%";
+const kFontNameFmtMonospace = "font.name.monospace.%LANG%";
+const kFontNameListFmtSerif = "font.name-list.serif.%LANG%";
+const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%";
+const kFontNameListFmtMonospace = "font.name-list.monospace.%LANG%";
+const kFontSizeFmtVariable = "font.size.variable.%LANG%";
+const kFontSizeFmtFixed = "font.size.monospace.%LANG%";
+const kFontMinSizeFmt = "font.minimum-size.%LANG%";
+
+document
+ .getElementById("FontsDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+window.addEventListener("load", () => gFontsDialog.onLoad());
+
+Preferences.addAll([
+ { id: "font.language.group", type: "wstring" },
+ { id: "browser.display.use_document_fonts", type: "int" },
+]);
+
+var gFontsDialog = {
+ _selectLanguageGroupPromise: Promise.resolve(),
+
+ onLoad() {
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("selectLangs"),
+ () => this.readFontLanguageGroup()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("useDocumentFonts"),
+ () => this.readUseDocumentFonts()
+ );
+ Preferences.addSyncToPrefListener(
+ document.getElementById("useDocumentFonts"),
+ () => this.writeUseDocumentFonts()
+ );
+ for (let id of ["serif", "sans-serif", "monospace"]) {
+ let el = document.getElementById(id);
+ Preferences.addSyncFromPrefListener(el, () =>
+ FontBuilder.readFontSelection(el)
+ );
+ }
+ },
+
+ _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;
+ },
+};
diff --git a/browser/components/preferences/dialogs/fonts.xhtml b/browser/components/preferences/dialogs/fonts.xhtml
new file mode 100644
index 0000000000..b5c9266384
--- /dev/null
+++ b/browser/components/preferences/dialogs/fonts.xhtml
@@ -0,0 +1,251 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.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="fonts-window"
+ data-l10n-attrs="title"
+ persist="lastSelected"
+>
+ <dialog
+ id="FontsDialog"
+ buttons="accept,cancel,help"
+ helpTopic="prefs-fonts-and-colors"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/fonts.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="fonts-window-close"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <!-- Fonts for: [ Language ] -->
+ <groupbox>
+ <hbox align="center">
+ <label control="selectLangs"
+ ><html:h2 data-l10n-id="fonts-langgroup-header"
+ /></label>
+ </hbox>
+ <menulist id="selectLangs" preference="font.language.group">
+ <menupopup>
+ <menuitem value="ar" data-l10n-id="fonts-langgroup-arabic" />
+ <menuitem value="x-armn" data-l10n-id="fonts-langgroup-armenian" />
+ <menuitem value="x-beng" data-l10n-id="fonts-langgroup-bengali" />
+ <menuitem
+ value="zh-CN"
+ data-l10n-id="fonts-langgroup-simpl-chinese"
+ />
+ <menuitem
+ value="zh-HK"
+ data-l10n-id="fonts-langgroup-trad-chinese-hk"
+ />
+ <menuitem value="zh-TW" data-l10n-id="fonts-langgroup-trad-chinese" />
+ <menuitem
+ value="x-cyrillic"
+ data-l10n-id="fonts-langgroup-cyrillic"
+ />
+ <menuitem
+ value="x-devanagari"
+ data-l10n-id="fonts-langgroup-devanagari"
+ />
+ <menuitem value="x-ethi" data-l10n-id="fonts-langgroup-ethiopic" />
+ <menuitem value="x-geor" data-l10n-id="fonts-langgroup-georgian" />
+ <menuitem value="el" data-l10n-id="fonts-langgroup-el" />
+ <menuitem value="x-gujr" data-l10n-id="fonts-langgroup-gujarati" />
+ <menuitem value="x-guru" data-l10n-id="fonts-langgroup-gurmukhi" />
+ <menuitem value="he" data-l10n-id="fonts-langgroup-hebrew" />
+ <menuitem value="ja" data-l10n-id="fonts-langgroup-japanese" />
+ <menuitem value="x-knda" data-l10n-id="fonts-langgroup-kannada" />
+ <menuitem value="x-khmr" data-l10n-id="fonts-langgroup-khmer" />
+ <menuitem value="ko" data-l10n-id="fonts-langgroup-korean" />
+ <menuitem value="x-western" data-l10n-id="fonts-langgroup-latin" />
+ <menuitem value="x-mlym" data-l10n-id="fonts-langgroup-malayalam" />
+ <menuitem value="x-math" data-l10n-id="fonts-langgroup-math" />
+ <menuitem value="x-orya" data-l10n-id="fonts-langgroup-odia" />
+ <menuitem value="x-sinh" data-l10n-id="fonts-langgroup-sinhala" />
+ <menuitem value="x-tamil" data-l10n-id="fonts-langgroup-tamil" />
+ <menuitem value="x-telu" data-l10n-id="fonts-langgroup-telugu" />
+ <menuitem value="th" data-l10n-id="fonts-langgroup-thai" />
+ <menuitem value="x-tibt" data-l10n-id="fonts-langgroup-tibetan" />
+ <menuitem value="x-cans" data-l10n-id="fonts-langgroup-canadian" />
+ <menuitem value="x-unicode" data-l10n-id="fonts-langgroup-other" />
+ </menupopup>
+ </menulist>
+
+ <separator class="thin" />
+
+ <box id="font-chooser-group">
+ <!-- proportional row -->
+ <hbox align="center" pack="end">
+ <label
+ data-l10n-id="fonts-proportional-header"
+ control="defaultFontType"
+ />
+ </hbox>
+ <menulist id="defaultFontType">
+ <menupopup>
+ <menuitem value="serif" data-l10n-id="fonts-default-serif" />
+ <menuitem
+ value="sans-serif"
+ data-l10n-id="fonts-default-sans-serif"
+ />
+ </menupopup>
+ </menulist>
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-proportional-size" control="sizeVar" />
+ </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 data-l10n-id="fonts-serif" control="serif" />
+ </hbox>
+ <menulist id="serif" delayprefsave="true" />
+ <spacer />
+ <spacer />
+
+ <!-- sans-serif row -->
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-sans-serif" control="sans-serif" />
+ </hbox>
+ <menulist id="sans-serif" delayprefsave="true" />
+ <spacer />
+ <spacer />
+
+ <!-- monospace row -->
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-monospace" control="monospace" />
+ </hbox>
+ <!--
+ FIXME(emilio): Why is this the only menulist here with crop="end"?
+ This goes back to the beginning of time...
+ -->
+ <menulist id="monospace" crop="end" delayprefsave="true" />
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-monospace-size" control="sizeMono" />
+ </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 align="center" pack="end">
+ <label data-l10n-id="fonts-minsize" control="minSize" />
+ <menulist id="minSize">
+ <menupopup>
+ <menuitem value="0" data-l10n-id="fonts-minsize-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>
+ <separator />
+ <separator class="groove" />
+ <hbox>
+ <checkbox
+ id="useDocumentFonts"
+ data-l10n-id="fonts-allow-own"
+ preference="browser.display.use_document_fonts"
+ />
+ </hbox>
+ </groupbox>
+
+ <!-- Load the script after the elements for layout issues (bug 1501755). -->
+ <script src="chrome://mozapps/content/preferences/fontbuilder.js" />
+ <script src="chrome://browser/content/preferences/dialogs/fonts.js" />
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/handlers.css b/browser/components/preferences/dialogs/handlers.css
new file mode 100644
index 0000000000..b53963fa4d
--- /dev/null
+++ b/browser/components/preferences/dialogs/handlers.css
@@ -0,0 +1,21 @@
+/* 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/. */
+
+/**
+ * Make the icons appear.
+ * Note: we display the icon box for every item whether or not it has an icon
+ * so the labels of all the items align vertically.
+ */
+.actionsMenu > menupopup > menuitem > .menu-iconic-left {
+ display: flex;
+ min-width: 16px;
+}
+
+/* Apply crisp rendering for favicons at exactly 2dppx resolution */
+@media (resolution: 2dppx) {
+ #handlersView > richlistitem,
+ .actionsMenu > menupopup > menuitem > .menu-iconic-left {
+ image-rendering: -moz-crisp-edges;
+ }
+}
diff --git a/browser/components/preferences/dialogs/jar.mn b/browser/components/preferences/dialogs/jar.mn
new file mode 100644
index 0000000000..a9f3dd1da2
--- /dev/null
+++ b/browser/components/preferences/dialogs/jar.mn
@@ -0,0 +1,49 @@
+# 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/.
+
+browser.jar:
+ content/browser/preferences/dialogs/addEngine.xhtml
+ content/browser/preferences/dialogs/addEngine.js
+ content/browser/preferences/dialogs/addEngine.css
+ content/browser/preferences/dialogs/applicationManager.xhtml
+ content/browser/preferences/dialogs/applicationManager.js
+ content/browser/preferences/dialogs/blocklists.xhtml
+ content/browser/preferences/dialogs/blocklists.js
+ content/browser/preferences/dialogs/browserLanguages.xhtml
+ content/browser/preferences/dialogs/browserLanguages.js
+ content/browser/preferences/dialogs/clearSiteData.css
+ content/browser/preferences/dialogs/clearSiteData.js
+ content/browser/preferences/dialogs/clearSiteData.xhtml
+ content/browser/preferences/dialogs/colors.xhtml
+ content/browser/preferences/dialogs/colors.js
+ content/browser/preferences/dialogs/connection.xhtml
+ content/browser/preferences/dialogs/connection.js
+ content/browser/preferences/dialogs/dohExceptions.xhtml
+ content/browser/preferences/dialogs/dohExceptions.js
+ content/browser/preferences/dialogs/fonts.xhtml
+ content/browser/preferences/dialogs/fonts.js
+ content/browser/preferences/dialogs/handlers.css
+ content/browser/preferences/dialogs/languages.xhtml
+ content/browser/preferences/dialogs/languages.js
+ content/browser/preferences/dialogs/permissions.xhtml
+ content/browser/preferences/dialogs/sitePermissions.xhtml
+ content/browser/preferences/dialogs/sitePermissions.js
+ content/browser/preferences/dialogs/sitePermissions.css
+ content/browser/preferences/dialogs/containers.xhtml
+ content/browser/preferences/dialogs/containers.js
+ content/browser/preferences/dialogs/permissions.js
+ content/browser/preferences/dialogs/sanitize.xhtml
+ content/browser/preferences/dialogs/sanitize.js
+ content/browser/preferences/dialogs/selectBookmark.xhtml
+ content/browser/preferences/dialogs/selectBookmark.js
+ content/browser/preferences/dialogs/siteDataSettings.xhtml
+ content/browser/preferences/dialogs/siteDataSettings.js
+* content/browser/preferences/dialogs/siteDataRemoveSelected.xhtml
+ content/browser/preferences/dialogs/siteDataRemoveSelected.js
+ content/browser/preferences/dialogs/syncChooseWhatToSync.xhtml
+ content/browser/preferences/dialogs/syncChooseWhatToSync.js
+ content/browser/preferences/dialogs/translationExceptions.xhtml
+ content/browser/preferences/dialogs/translationExceptions.js
+ content/browser/preferences/dialogs/translations.xhtml
+ content/browser/preferences/dialogs/translations.js
diff --git a/browser/components/preferences/dialogs/languages.js b/browser/components/preferences/dialogs/languages.js
new file mode 100644
index 0000000000..502a083c8b
--- /dev/null
+++ b/browser/components/preferences/dialogs/languages.js
@@ -0,0 +1,384 @@
+/* -*- 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 */
+
+document
+ .getElementById("LanguagesDialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+Preferences.addAll([
+ { id: "intl.accept_languages", type: "wstring" },
+ { id: "pref.browser.language.disable_button.up", type: "bool" },
+ { id: "pref.browser.language.disable_button.down", type: "bool" },
+ { id: "pref.browser.language.disable_button.remove", type: "bool" },
+ { id: "privacy.spoof_english", type: "int" },
+]);
+
+var gLanguagesDialog = {
+ _availableLanguagesList: [],
+ _acceptLanguages: {},
+
+ _selectedItemID: null,
+
+ onLoad() {
+ let spoofEnglishElement = document.getElementById("spoofEnglish");
+ Preferences.addSyncFromPrefListener(spoofEnglishElement, () =>
+ gLanguagesDialog.readSpoofEnglish()
+ );
+ Preferences.addSyncToPrefListener(spoofEnglishElement, () =>
+ gLanguagesDialog.writeSpoofEnglish()
+ );
+
+ Preferences.get("intl.accept_languages").on("change", () =>
+ this._readAcceptLanguages().catch(console.error)
+ );
+
+ if (!this._availableLanguagesList.length) {
+ document.mozSubdialogReady = this._loadAvailableLanguages();
+ }
+ },
+
+ get _activeLanguages() {
+ return document.getElementById("activeLanguages");
+ },
+
+ get _availableLanguages() {
+ return document.getElementById("availableLanguages");
+ },
+
+ async _loadAvailableLanguages() {
+ // This is a parser for: resource://gre/res/language.properties
+ // The file is formatted like so:
+ // ab[-cd].accept=true|false
+ // ab = language
+ // cd = region
+ var bundleAccepted = document.getElementById("bundleAccepted");
+
+ function LocaleInfo(aLocaleName, aLocaleCode, aIsVisible) {
+ this.name = aLocaleName;
+ this.code = aLocaleCode;
+ this.isVisible = aIsVisible;
+ }
+
+ // 1) Read the available languages out of language.properties
+
+ let localeCodes = [];
+ let localeValues = [];
+ for (let currString of bundleAccepted.strings) {
+ var property = currString.key.split("."); // ab[-cd].accept
+ if (property[1] == "accept") {
+ localeCodes.push(property[0]);
+ localeValues.push(currString.value);
+ }
+ }
+
+ let localeNames = Services.intl.getLocaleDisplayNames(
+ undefined,
+ localeCodes
+ );
+
+ for (let i in localeCodes) {
+ let isVisible =
+ localeValues[i] == "true" &&
+ (!(localeCodes[i] in this._acceptLanguages) ||
+ !this._acceptLanguages[localeCodes[i]]);
+
+ let li = new LocaleInfo(localeNames[i], localeCodes[i], isVisible);
+ this._availableLanguagesList.push(li);
+ }
+
+ await this._buildAvailableLanguageList();
+ await this._readAcceptLanguages();
+ },
+
+ async _buildAvailableLanguageList() {
+ var availableLanguagesPopup = document.getElementById(
+ "availableLanguagesPopup"
+ );
+ while (availableLanguagesPopup.hasChildNodes()) {
+ availableLanguagesPopup.firstChild.remove();
+ }
+
+ let frag = document.createDocumentFragment();
+
+ // Load the UI with the data
+ for (var i = 0; i < this._availableLanguagesList.length; ++i) {
+ let locale = this._availableLanguagesList[i];
+ let localeCode = locale.code;
+ if (
+ locale.isVisible &&
+ (!(localeCode in this._acceptLanguages) ||
+ !this._acceptLanguages[localeCode])
+ ) {
+ var menuitem = document.createXULElement("menuitem");
+ menuitem.id = localeCode;
+ document.l10n.setAttributes(menuitem, "languages-code-format", {
+ locale: locale.name,
+ code: localeCode,
+ });
+ frag.appendChild(menuitem);
+ }
+ }
+
+ await document.l10n.translateFragment(frag);
+
+ // Sort the list of languages by name
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ let items = Array.from(frag.children);
+
+ items.sort((a, b) => {
+ return comp.compare(a.getAttribute("label"), b.getAttribute("label"));
+ });
+
+ // Re-append items in the correct order:
+ items.forEach(item => frag.appendChild(item));
+
+ availableLanguagesPopup.appendChild(frag);
+
+ this._availableLanguages.setAttribute(
+ "label",
+ this._availableLanguages.getAttribute("placeholder")
+ );
+ },
+
+ async _readAcceptLanguages() {
+ while (this._activeLanguages.hasChildNodes()) {
+ this._activeLanguages.firstChild.remove();
+ }
+
+ var selectedIndex = 0;
+ var preference = Preferences.get("intl.accept_languages");
+ if (preference.value == "") {
+ return;
+ }
+ var languages = preference.value.toLowerCase().split(/\s*,\s*/);
+ for (var i = 0; i < languages.length; ++i) {
+ var listitem = document.createXULElement("richlistitem");
+ var label = document.createXULElement("label");
+ listitem.appendChild(label);
+ listitem.id = languages[i];
+ if (languages[i] == this._selectedItemID) {
+ selectedIndex = i;
+ }
+ this._activeLanguages.appendChild(listitem);
+ var localeName = this._getLocaleName(languages[i]);
+ document.l10n.setAttributes(label, "languages-active-code-format", {
+ locale: localeName,
+ code: languages[i],
+ });
+
+ // Hash this language as an "Active" language so we don't
+ // show it in the list that can be added.
+ this._acceptLanguages[languages[i]] = true;
+ }
+
+ // We're forcing an early localization here because otherwise
+ // the initial sizing of the dialog will happen before it and
+ // result in overflow.
+ await document.l10n.translateFragment(this._activeLanguages);
+
+ if (this._activeLanguages.childNodes.length) {
+ this._activeLanguages.ensureIndexIsVisible(selectedIndex);
+ this._activeLanguages.selectedIndex = selectedIndex;
+ }
+
+ // Update states of accept-language list and buttons according to
+ // privacy.resistFingerprinting and privacy.spoof_english.
+ this.readSpoofEnglish();
+ },
+
+ onAvailableLanguageSelect() {
+ var availableLanguages = this._availableLanguages;
+ var addButton = document.getElementById("addButton");
+ addButton.disabled =
+ availableLanguages.disabled || availableLanguages.selectedIndex < 0;
+
+ this._availableLanguages.removeAttribute("accesskey");
+ },
+
+ addLanguage() {
+ var selectedID = this._availableLanguages.selectedItem.id;
+ var preference = Preferences.get("intl.accept_languages");
+ var arrayOfPrefs = preference.value.toLowerCase().split(/\s*,\s*/);
+ for (var i = 0; i < arrayOfPrefs.length; ++i) {
+ if (arrayOfPrefs[i] == selectedID) {
+ return;
+ }
+ }
+
+ this._selectedItemID = selectedID;
+
+ if (preference.value == "") {
+ preference.value = selectedID;
+ } else {
+ arrayOfPrefs.unshift(selectedID);
+ preference.value = arrayOfPrefs.join(",");
+ }
+
+ this._acceptLanguages[selectedID] = true;
+ this._availableLanguages.selectedItem = null;
+
+ // Rebuild the available list with the added item removed...
+ this._buildAvailableLanguageList().catch(console.error);
+ },
+
+ removeLanguage() {
+ // Build the new preference value string.
+ var languagesArray = [];
+ for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) {
+ var item = this._activeLanguages.childNodes[i];
+ if (!item.selected) {
+ languagesArray.push(item.id);
+ } else {
+ this._acceptLanguages[item.id] = false;
+ }
+ }
+ var string = languagesArray.join(",");
+
+ // Get the item to select after the remove operation completes.
+ var selection = this._activeLanguages.selectedItems;
+ var lastSelected = selection[selection.length - 1];
+ var selectItem = lastSelected.nextSibling || lastSelected.previousSibling;
+ selectItem = selectItem ? selectItem.id : null;
+
+ this._selectedItemID = selectItem;
+
+ // Update the preference and force a UI rebuild
+ var preference = Preferences.get("intl.accept_languages");
+ preference.value = string;
+
+ this._buildAvailableLanguageList().catch(console.error);
+ },
+
+ _getLocaleName(localeCode) {
+ if (!this._availableLanguagesList.length) {
+ this._loadAvailableLanguages();
+ }
+ let languageName = "";
+ for (var i = 0; i < this._availableLanguagesList.length; ++i) {
+ if (localeCode == this._availableLanguagesList[i].code) {
+ return this._availableLanguagesList[i].name;
+ }
+ // Try resolving the locale code without region code. Can't return
+ // directly because there might be a perfect match later.
+ if (localeCode.split("-")[0] == this._availableLanguagesList[i].code) {
+ languageName = this._availableLanguagesList[i].name;
+ }
+ }
+
+ return languageName;
+ },
+
+ moveUp() {
+ var selectedItem = this._activeLanguages.selectedItems[0];
+ var previousItem = selectedItem.previousSibling;
+
+ var string = "";
+ for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) {
+ var item = this._activeLanguages.childNodes[i];
+ string += i == 0 ? "" : ",";
+ if (item.id == previousItem.id) {
+ string += selectedItem.id;
+ } else if (item.id == selectedItem.id) {
+ string += previousItem.id;
+ } else {
+ string += item.id;
+ }
+ }
+
+ this._selectedItemID = selectedItem.id;
+
+ // Update the preference and force a UI rebuild
+ var preference = Preferences.get("intl.accept_languages");
+ preference.value = string;
+ },
+
+ moveDown() {
+ var selectedItem = this._activeLanguages.selectedItems[0];
+ var nextItem = selectedItem.nextSibling;
+
+ var string = "";
+ for (var i = 0; i < this._activeLanguages.childNodes.length; ++i) {
+ var item = this._activeLanguages.childNodes[i];
+ string += i == 0 ? "" : ",";
+ if (item.id == nextItem.id) {
+ string += selectedItem.id;
+ } else if (item.id == selectedItem.id) {
+ string += nextItem.id;
+ } else {
+ string += item.id;
+ }
+ }
+
+ this._selectedItemID = selectedItem.id;
+
+ // Update the preference and force a UI rebuild
+ var preference = Preferences.get("intl.accept_languages");
+ preference.value = string;
+ },
+
+ onLanguageSelect() {
+ var upButton = document.getElementById("up");
+ var downButton = document.getElementById("down");
+ var removeButton = document.getElementById("remove");
+ switch (this._activeLanguages.selectedCount) {
+ case 0:
+ upButton.disabled = downButton.disabled = removeButton.disabled = true;
+ break;
+ case 1:
+ upButton.disabled = this._activeLanguages.selectedIndex == 0;
+ downButton.disabled =
+ this._activeLanguages.selectedIndex ==
+ this._activeLanguages.childNodes.length - 1;
+ removeButton.disabled = false;
+ break;
+ default:
+ upButton.disabled = true;
+ downButton.disabled = true;
+ removeButton.disabled = false;
+ }
+ },
+
+ readSpoofEnglish() {
+ var checkbox = document.getElementById("spoofEnglish");
+ var resistFingerprinting = Services.prefs.getBoolPref(
+ "privacy.resistFingerprinting"
+ );
+ if (!resistFingerprinting) {
+ checkbox.hidden = true;
+ return false;
+ }
+
+ var spoofEnglish = Preferences.get("privacy.spoof_english").value;
+ var activeLanguages = this._activeLanguages;
+ var availableLanguages = this._availableLanguages;
+ checkbox.hidden = false;
+ switch (spoofEnglish) {
+ case 1: // don't spoof intl.accept_languages
+ activeLanguages.disabled = false;
+ activeLanguages.selectItem(activeLanguages.firstChild);
+ availableLanguages.disabled = false;
+ this.onAvailableLanguageSelect();
+ return false;
+ case 2: // spoof intl.accept_languages
+ activeLanguages.clearSelection();
+ activeLanguages.disabled = true;
+ availableLanguages.disabled = true;
+ this.onAvailableLanguageSelect();
+ return true;
+ default:
+ // will prompt for spoofing intl.accept_languages if resisting fingerprinting
+ return false;
+ }
+ },
+
+ writeSpoofEnglish() {
+ return document.getElementById("spoofEnglish").checked ? 2 : 1;
+ },
+};
diff --git a/browser/components/preferences/dialogs/languages.xhtml b/browser/components/preferences/dialogs/languages.xhtml
new file mode 100644
index 0000000000..bd509e84e4
--- /dev/null
+++ b/browser/components/preferences/dialogs/languages.xhtml
@@ -0,0 +1,104 @@
+<?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://browser/skin/preferences/preferences.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="webpage-languages-window2"
+ data-l10n-attrs="title, style"
+ persist="lastSelected"
+ onload="gLanguagesDialog.onLoad();"
+>
+ <dialog
+ id="LanguagesDialog"
+ buttons="accept,cancel,help"
+ helpTopic="prefs-languages"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/languages.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/dialogs/languages.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="languages-close-key"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <stringbundleset id="languageSet">
+ <stringbundle
+ id="bundleAccepted"
+ src="resource://gre/res/language.properties"
+ />
+ </stringbundleset>
+
+ <description data-l10n-id="languages-description" />
+ <checkbox
+ id="spoofEnglish"
+ data-l10n-id="languages-customize-spoof-english"
+ preference="privacy.spoof_english"
+ />
+ <box class="languages-grid">
+ <richlistbox
+ id="activeLanguages"
+ seltype="multiple"
+ onselect="gLanguagesDialog.onLanguageSelect();"
+ />
+ <vbox>
+ <button
+ id="up"
+ class="up"
+ oncommand="gLanguagesDialog.moveUp();"
+ disabled="true"
+ data-l10n-id="languages-customize-moveup"
+ preference="pref.browser.language.disable_button.up"
+ />
+ <button
+ id="down"
+ class="down"
+ oncommand="gLanguagesDialog.moveDown();"
+ disabled="true"
+ data-l10n-id="languages-customize-movedown"
+ preference="pref.browser.language.disable_button.down"
+ />
+ <button
+ id="remove"
+ oncommand="gLanguagesDialog.removeLanguage();"
+ disabled="true"
+ data-l10n-id="languages-customize-remove"
+ preference="pref.browser.language.disable_button.remove"
+ />
+ </vbox>
+ <!-- This <vbox> is needed to position search tooltips correctly. -->
+ <vbox>
+ <menulist
+ id="availableLanguages"
+ oncommand="gLanguagesDialog.onAvailableLanguageSelect();"
+ data-l10n-id="languages-customize-select-language"
+ data-l10n-attrs="placeholder"
+ >
+ <menupopup id="availableLanguagesPopup" />
+ </menulist>
+ </vbox>
+ <button
+ id="addButton"
+ class="add-web-language"
+ oncommand="gLanguagesDialog.addLanguage();"
+ disabled="true"
+ data-l10n-id="languages-customize-add"
+ />
+ </box>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/moz.build b/browser/components/preferences/dialogs/moz.build
new file mode 100644
index 0000000000..603c560505
--- /dev/null
+++ b/browser/components/preferences/dialogs/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+for var in ("MOZ_APP_NAME", "MOZ_MACBUNDLE_NAME"):
+ DEFINES[var] = CONFIG[var]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("windows", "gtk", "cocoa"):
+ DEFINES["HAVE_SHELL_SERVICE"] = 1
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/browser/components/preferences/dialogs/permissions.js b/browser/components/preferences/dialogs/permissions.js
new file mode 100644
index 0000000000..3ca2664611
--- /dev/null
+++ b/browser/components/preferences/dialogs/permissions.js
@@ -0,0 +1,645 @@
+/* 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 { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "contentBlockingAllowList",
+ "@mozilla.org/content-blocking-allow-list;1",
+ "nsIContentBlockingAllowList"
+);
+
+const permissionExceptionsL10n = {
+ trackingprotection: {
+ window: "permissions-exceptions-etp-window2",
+ description: "permissions-exceptions-manage-etp-desc",
+ },
+ cookie: {
+ window: "permissions-exceptions-cookie-window2",
+ description: "permissions-exceptions-cookie-desc",
+ },
+ popup: {
+ window: "permissions-exceptions-popup-window2",
+ description: "permissions-exceptions-popup-desc",
+ },
+ "login-saving": {
+ window: "permissions-exceptions-saved-logins-window2",
+ description: "permissions-exceptions-saved-logins-desc",
+ },
+ "https-only-load-insecure": {
+ window: "permissions-exceptions-https-only-window2",
+ description: "permissions-exceptions-https-only-desc",
+ },
+ install: {
+ window: "permissions-exceptions-addons-window2",
+ description: "permissions-exceptions-addons-desc",
+ },
+};
+
+function Permission(principal, type, capability) {
+ this.principal = principal;
+ this.origin = principal.origin;
+ this.type = type;
+ this.capability = capability;
+}
+
+var gPermissionManager = {
+ _type: "",
+ _isObserving: false,
+ _permissions: new Map(),
+ _permissionsToAdd: new Map(),
+ _permissionsToDelete: new Map(),
+ _bundle: null,
+ _list: null,
+ _removeButton: null,
+ _removeAllButton: null,
+
+ onLoad() {
+ let params = window.arguments[0];
+ document.mozSubdialogReady = this.init(params);
+ },
+
+ async init(params) {
+ if (!this._isObserving) {
+ Services.obs.addObserver(this, "perm-changed");
+ this._isObserving = true;
+ }
+
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+
+ this._type = params.permissionType;
+ this._list = document.getElementById("permissionsBox");
+ this._removeButton = document.getElementById("removePermission");
+ this._removeAllButton = document.getElementById("removeAllPermissions");
+
+ this._btnCookieSession = document.getElementById("btnCookieSession");
+ this._btnBlock = document.getElementById("btnBlock");
+ this._btnDisableETP = document.getElementById("btnDisableETP");
+ this._btnAllow = document.getElementById("btnAllow");
+ this._btnHttpsOnlyOff = document.getElementById("btnHttpsOnlyOff");
+ this._btnHttpsOnlyOffTmp = document.getElementById("btnHttpsOnlyOffTmp");
+
+ let permissionsText = document.getElementById("permissionsText");
+
+ let l10n = permissionExceptionsL10n[this._type];
+ document.l10n.setAttributes(permissionsText, l10n.description);
+ document.l10n.setAttributes(document.documentElement, l10n.window);
+
+ let urlFieldVisible =
+ params.blockVisible ||
+ params.sessionVisible ||
+ params.allowVisible ||
+ params.disableETPVisible;
+
+ this._urlField = document.getElementById("url");
+ this._urlField.value = params.prefilledHost;
+ this._urlField.hidden = !urlFieldVisible;
+
+ await document.l10n.translateElements([
+ permissionsText,
+ document.documentElement,
+ ]);
+
+ document.getElementById("btnDisableETP").hidden = !params.disableETPVisible;
+ document.getElementById("btnBlock").hidden = !params.blockVisible;
+ document.getElementById("btnCookieSession").hidden = !(
+ params.sessionVisible && this._type == "cookie"
+ );
+ document.getElementById("btnHttpsOnlyOff").hidden = !(
+ this._type == "https-only-load-insecure"
+ );
+ document.getElementById("btnHttpsOnlyOffTmp").hidden = !(
+ params.sessionVisible && this._type == "https-only-load-insecure"
+ );
+ document.getElementById("btnAllow").hidden = !params.allowVisible;
+
+ this.onHostInput(this._urlField);
+
+ let urlLabel = document.getElementById("urlLabel");
+ urlLabel.hidden = !urlFieldVisible;
+
+ this._hideStatusColumn = params.hideStatusColumn;
+ let statusCol = document.getElementById("statusCol");
+ statusCol.hidden = this._hideStatusColumn;
+ if (this._hideStatusColumn) {
+ statusCol.removeAttribute("data-isCurrentSortCol");
+ document
+ .getElementById("siteCol")
+ .setAttribute("data-isCurrentSortCol", "true");
+ }
+
+ Services.obs.notifyObservers(null, "flush-pending-permissions", this._type);
+
+ this._loadPermissions();
+ this.buildPermissionsList();
+
+ this._urlField.focus();
+ },
+
+ uninit() {
+ if (this._isObserving) {
+ Services.obs.removeObserver(this, "perm-changed");
+ this._isObserving = false;
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic !== "perm-changed") {
+ return;
+ }
+
+ let permission = subject.QueryInterface(Ci.nsIPermission);
+
+ // Ignore unrelated permission types.
+ if (permission.type !== this._type) {
+ return;
+ }
+
+ if (data == "added") {
+ this._addPermissionToList(permission);
+ this.buildPermissionsList();
+ } else if (data == "changed") {
+ let p = this._permissions.get(permission.principal.origin);
+ // Maybe this item has been excluded before because it had an invalid capability.
+ if (p) {
+ p.capability = permission.capability;
+ this._handleCapabilityChange(p);
+ } else {
+ this._addPermissionToList(permission);
+ }
+ this.buildPermissionsList();
+ } else if (data == "deleted") {
+ this._removePermissionFromList(permission.principal.origin);
+ }
+ },
+
+ _handleCapabilityChange(perm) {
+ let permissionlistitem = document.getElementsByAttribute(
+ "origin",
+ perm.origin
+ )[0];
+ document.l10n.setAttributes(
+ permissionlistitem.querySelector(".website-capability-value"),
+ this._getCapabilityL10nId(perm.capability)
+ );
+ },
+
+ _isCapabilitySupported(capability) {
+ return (
+ capability == Ci.nsIPermissionManager.ALLOW_ACTION ||
+ capability == Ci.nsIPermissionManager.DENY_ACTION ||
+ capability == Ci.nsICookiePermission.ACCESS_SESSION ||
+ // Bug 1753600 there are still a few legacy cookies around that have the capability 9,
+ // _getCapabilityL10nId will throw if it receives a capability of 9
+ // that is not in combination with the type https-only-load-insecure
+ (capability ==
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION &&
+ this._type == "https-only-load-insecure")
+ );
+ },
+
+ _getCapabilityL10nId(capability) {
+ // HTTPS-Only Mode phrases exceptions as turning it off
+ if (this._type == "https-only-load-insecure") {
+ return this._getHttpsOnlyCapabilityL10nId(capability);
+ }
+
+ switch (capability) {
+ case Ci.nsIPermissionManager.ALLOW_ACTION:
+ return "permissions-capabilities-listitem-allow";
+ case Ci.nsIPermissionManager.DENY_ACTION:
+ return "permissions-capabilities-listitem-block";
+ case Ci.nsICookiePermission.ACCESS_SESSION:
+ return "permissions-capabilities-listitem-allow-session";
+ default:
+ throw new Error(`Unknown capability: ${capability}`);
+ }
+ },
+
+ _getHttpsOnlyCapabilityL10nId(capability) {
+ switch (capability) {
+ case Ci.nsIPermissionManager.ALLOW_ACTION:
+ return "permissions-capabilities-listitem-off";
+ case Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION:
+ return "permissions-capabilities-listitem-off-temporarily";
+ default:
+ throw new Error(`Unknown HTTPS-Only Mode capability: ${capability}`);
+ }
+ },
+
+ _addPermissionToList(perm) {
+ if (perm.type !== this._type) {
+ return;
+ }
+ if (!this._isCapabilitySupported(perm.capability)) {
+ return;
+ }
+
+ // Skip private browsing session permissions.
+ if (
+ perm.principal.privateBrowsingId !==
+ Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID &&
+ perm.expireType === Services.perms.EXPIRE_SESSION
+ ) {
+ return;
+ }
+
+ let p = new Permission(perm.principal, perm.type, perm.capability);
+ this._permissions.set(p.origin, p);
+ },
+
+ _addOrModifyPermission(principal, capability) {
+ // check whether the permission already exists, if not, add it
+ let permissionParams = { principal, type: this._type, capability };
+ let existingPermission = this._permissions.get(principal.origin);
+ if (!existingPermission) {
+ this._permissionsToAdd.set(principal.origin, permissionParams);
+ this._addPermissionToList(permissionParams);
+ this.buildPermissionsList();
+ } else if (existingPermission.capability != capability) {
+ existingPermission.capability = capability;
+ this._permissionsToAdd.set(principal.origin, permissionParams);
+ this._handleCapabilityChange(existingPermission);
+ }
+ },
+
+ _addNewPrincipalToList(list, uri) {
+ list.push(Services.scriptSecurityManager.createContentPrincipal(uri, {}));
+ // If we have ended up with an unknown scheme, the following will throw.
+ list[list.length - 1].origin;
+ },
+
+ addPermission(capability) {
+ let textbox = document.getElementById("url");
+ let input_url = textbox.value.trim(); // trim any leading and trailing space
+ let principals = [];
+ 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.
+ try {
+ let uri = Services.io.newURI(input_url);
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ if (principal.origin.startsWith("moz-nullprincipal:")) {
+ throw new Error("Null principal");
+ }
+ principals.push(principal);
+ } catch (ex) {
+ this._addNewPrincipalToList(
+ principals,
+ Services.io.newURI("http://" + input_url)
+ );
+ this._addNewPrincipalToList(
+ principals,
+ Services.io.newURI("https://" + input_url)
+ );
+ }
+ } catch (ex) {
+ document.l10n
+ .formatValues([
+ { id: "permissions-invalid-uri-title" },
+ { id: "permissions-invalid-uri-label" },
+ ])
+ .then(([title, message]) => {
+ Services.prompt.alert(window, title, message);
+ });
+ return;
+ }
+ // In case of an ETP exception we compute the contentBlockingAllowList principal
+ // to align with the allow list behavior triggered by the protections panel
+ if (this._type == "trackingprotection") {
+ principals = principals.map(
+ lazy.contentBlockingAllowList.computeContentBlockingAllowListPrincipal
+ );
+ }
+ for (let principal of principals) {
+ this._addOrModifyPermission(principal, capability);
+ }
+
+ 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
+ this._setRemoveButtonState();
+ },
+
+ _removePermission(permission) {
+ this._removePermissionFromList(permission.origin);
+
+ // 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(permission.origin);
+ if (!isNewPermission) {
+ this._permissionsToDelete.set(permission.origin, permission);
+ }
+ },
+
+ _removePermissionFromList(origin) {
+ this._permissions.delete(origin);
+ let permissionlistitem = document.getElementsByAttribute(
+ "origin",
+ origin
+ )[0];
+ if (permissionlistitem) {
+ permissionlistitem.remove();
+ }
+ },
+
+ _loadPermissions() {
+ // load permissions into a table.
+ for (let nextPermission of Services.perms.all) {
+ this._addPermissionToList(nextPermission);
+ }
+ },
+
+ _createPermissionListItem(permission) {
+ let disabledByPolicy = this._permissionDisabledByPolicy(permission);
+ let richlistitem = document.createXULElement("richlistitem");
+ richlistitem.setAttribute("origin", permission.origin);
+ let row = document.createXULElement("hbox");
+ row.setAttribute("style", "flex: 1");
+
+ let hbox = document.createXULElement("hbox");
+ let website = document.createXULElement("label");
+ website.setAttribute("disabled", disabledByPolicy);
+ website.setAttribute("class", "website-name-value");
+ website.setAttribute("value", permission.origin);
+ hbox.setAttribute("class", "website-name");
+ hbox.setAttribute("style", "flex: 3 3; width: 0");
+ hbox.appendChild(website);
+ row.appendChild(hbox);
+
+ if (!this._hideStatusColumn) {
+ hbox = document.createXULElement("hbox");
+ let capability = document.createXULElement("label");
+ capability.setAttribute("disabled", disabledByPolicy);
+ capability.setAttribute("class", "website-capability-value");
+ document.l10n.setAttributes(
+ capability,
+ this._getCapabilityL10nId(permission.capability)
+ );
+ hbox.setAttribute("class", "website-name");
+ hbox.setAttribute("style", "flex: 1; width: 0");
+ hbox.appendChild(capability);
+ row.appendChild(hbox);
+ }
+
+ richlistitem.appendChild(row);
+ return richlistitem;
+ },
+
+ onWindowKeyPress(event) {
+ // Prevent dialog.js from closing the dialog when the user submits the input
+ // field via the return key.
+ if (
+ event.keyCode == KeyEvent.DOM_VK_RETURN &&
+ document.activeElement == this._urlField
+ ) {
+ event.preventDefault();
+ }
+ },
+
+ onPermissionKeyPress(event) {
+ if (!this._list.selectedItem) {
+ return;
+ }
+
+ if (
+ event.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+ ) {
+ this.onPermissionDelete();
+ event.preventDefault();
+ }
+ },
+
+ onHostKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
+ if (!document.getElementById("btnAllow").hidden) {
+ document.getElementById("btnAllow").click();
+ } else if (!document.getElementById("btnBlock").hidden) {
+ document.getElementById("btnBlock").click();
+ } else if (!document.getElementById("btnHttpsOnlyOff").hidden) {
+ document.getElementById("btnHttpsOnlyOff").click();
+ } else if (!document.getElementById("btnDisableETP").hidden) {
+ document.getElementById("btnDisableETP").click();
+ }
+ }
+ },
+
+ onHostInput(siteField) {
+ this._btnCookieSession.disabled =
+ this._btnCookieSession.hidden || !siteField.value;
+ this._btnHttpsOnlyOff.disabled =
+ this._btnHttpsOnlyOff.hidden || !siteField.value;
+ this._btnHttpsOnlyOffTmp.disabled =
+ this._btnHttpsOnlyOffTmp.hidden || !siteField.value;
+ this._btnBlock.disabled = this._btnBlock.hidden || !siteField.value;
+ this._btnDisableETP.disabled =
+ this._btnDisableETP.hidden || !siteField.value;
+ this._btnAllow.disabled = this._btnAllow.hidden || !siteField.value;
+ },
+
+ _setRemoveButtonState() {
+ if (!this._list) {
+ return;
+ }
+
+ let hasSelection = this._list.selectedIndex >= 0;
+
+ let disabledByPolicy = false;
+ if (Services.policies.status === Services.policies.ACTIVE && hasSelection) {
+ let origin = this._list.selectedItem.getAttribute("origin");
+ disabledByPolicy = this._permissionDisabledByPolicy(
+ this._permissions.get(origin)
+ );
+ }
+
+ this._removeButton.disabled = !hasSelection || disabledByPolicy;
+ let disabledItems = this._list.querySelectorAll(
+ "label.website-name-value[disabled='true']"
+ );
+
+ this._removeAllButton.disabled =
+ this._list.itemCount == disabledItems.length;
+ },
+
+ onPermissionDelete() {
+ let richlistitem = this._list.selectedItem;
+ let origin = richlistitem.getAttribute("origin");
+ let permission = this._permissions.get(origin);
+ if (this._permissionDisabledByPolicy(permission)) {
+ return;
+ }
+
+ this._removePermission(permission);
+
+ this._setRemoveButtonState();
+ },
+
+ onAllPermissionsDelete() {
+ for (let permission of this._permissions.values()) {
+ if (this._permissionDisabledByPolicy(permission)) {
+ continue;
+ }
+ this._removePermission(permission);
+ }
+
+ this._setRemoveButtonState();
+ },
+
+ onPermissionSelect() {
+ this._setRemoveButtonState();
+ },
+
+ 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 p of this._permissionsToDelete.values()) {
+ Services.perms.removeFromPrincipal(p.principal, p.type);
+ }
+
+ for (let p of this._permissionsToAdd.values()) {
+ // If this sets the HTTPS-Only exemption only for this
+ // session, then the expire-type has to be set.
+ if (
+ p.capability ==
+ Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION
+ ) {
+ Services.perms.addFromPrincipal(
+ p.principal,
+ p.type,
+ p.capability,
+ Ci.nsIPermissionManager.EXPIRE_SESSION
+ );
+ } else {
+ Services.perms.addFromPrincipal(p.principal, p.type, p.capability);
+ }
+ }
+ },
+
+ buildPermissionsList(sortCol) {
+ // Clear old entries.
+ let oldItems = this._list.querySelectorAll("richlistitem");
+ for (let item of oldItems) {
+ item.remove();
+ }
+ let frag = document.createDocumentFragment();
+
+ let permissions = Array.from(this._permissions.values());
+
+ for (let permission of permissions) {
+ let richlistitem = this._createPermissionListItem(permission);
+ frag.appendChild(richlistitem);
+ }
+
+ // Sort permissions.
+ this._sortPermissions(this._list, frag, sortCol);
+
+ this._list.appendChild(frag);
+
+ this._setRemoveButtonState();
+ },
+
+ _permissionDisabledByPolicy(permission) {
+ let permissionObject = Services.perms.getPermissionObject(
+ permission.principal,
+ this._type,
+ false
+ );
+ return (
+ permissionObject?.expireType == Ci.nsIPermissionManager.EXPIRE_POLICY
+ );
+ },
+
+ _sortPermissions(list, frag, column) {
+ let sortDirection;
+
+ if (!column) {
+ column = document.querySelector("treecol[data-isCurrentSortCol=true]");
+ sortDirection =
+ column.getAttribute("data-last-sortDirection") || "ascending";
+ } else {
+ sortDirection = column.getAttribute("data-last-sortDirection");
+ sortDirection =
+ sortDirection === "ascending" ? "descending" : "ascending";
+ }
+
+ let sortFunc = null;
+ switch (column.id) {
+ case "siteCol":
+ sortFunc = (a, b) => {
+ return comp.compare(
+ a.getAttribute("origin"),
+ b.getAttribute("origin")
+ );
+ };
+ break;
+
+ case "statusCol":
+ sortFunc = (a, b) => {
+ // The capabilities values ("Allow" and "Block") are localized asynchronously.
+ // Sort based on the guaranteed-present localization ID instead, note that the
+ // ascending/descending arrow may be pointing the wrong way.
+ return (
+ a
+ .querySelector(".website-capability-value")
+ .getAttribute("data-l10n-id") >
+ b
+ .querySelector(".website-capability-value")
+ .getAttribute("data-l10n-id")
+ );
+ };
+ break;
+ }
+
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ let items = Array.from(frag.querySelectorAll("richlistitem"));
+
+ if (sortDirection === "descending") {
+ items.sort((a, b) => sortFunc(b, a));
+ } else {
+ items.sort(sortFunc);
+ }
+
+ // Re-append items in the correct order:
+ items.forEach(item => frag.appendChild(item));
+
+ let cols = list.previousElementSibling.querySelectorAll("treecol");
+ cols.forEach(c => {
+ c.removeAttribute("data-isCurrentSortCol");
+ c.removeAttribute("sortDirection");
+ });
+ column.setAttribute("data-isCurrentSortCol", "true");
+ column.setAttribute("sortDirection", sortDirection);
+ column.setAttribute("data-last-sortDirection", sortDirection);
+ },
+};
diff --git a/browser/components/preferences/dialogs/permissions.xhtml b/browser/components/preferences/dialogs/permissions.xhtml
new file mode 100644
index 0000000000..13d756bae0
--- /dev/null
+++ b/browser/components/preferences/dialogs/permissions.xhtml
@@ -0,0 +1,134 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/sitePermissions.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="PermissionsDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="permissions-window2"
+ data-l10n-attrs="title, style"
+ onload="gPermissionManager.onLoad();"
+ onunload="gPermissionManager.uninit();"
+ persist="width height"
+ onkeypress="gPermissionManager.onWindowKeyPress(event);"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="permission-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/permissions.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/permissions.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="permissions-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <description id="permissionsText" control="url" />
+ <separator class="thin" />
+ <label id="urlLabel" control="url" data-l10n-id="permissions-address" />
+ <hbox align="start">
+ <html:input
+ id="url"
+ type="text"
+ style="flex: 1"
+ oninput="gPermissionManager.onHostInput(event.target);"
+ onkeypress="gPermissionManager.onHostKeyPress(event);"
+ />
+ </hbox>
+ <hbox pack="end">
+ <button
+ id="btnDisableETP"
+ disabled="true"
+ data-l10n-id="permissions-disable-etp"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);"
+ />
+ <button
+ id="btnBlock"
+ disabled="true"
+ data-l10n-id="permissions-block"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.DENY_ACTION);"
+ />
+ <button
+ id="btnCookieSession"
+ disabled="true"
+ data-l10n-id="permissions-session"
+ oncommand="gPermissionManager.addPermission(Ci.nsICookiePermission.ACCESS_SESSION);"
+ />
+ <button
+ id="btnAllow"
+ disabled="true"
+ data-l10n-id="permissions-allow"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);"
+ />
+ <button
+ id="btnHttpsOnlyOff"
+ disabled="true"
+ data-l10n-id="permissions-button-off"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);"
+ />
+ <button
+ id="btnHttpsOnlyOffTmp"
+ disabled="true"
+ data-l10n-id="permissions-button-off-temporarily"
+ oncommand="gPermissionManager.addPermission(Ci.nsIHttpsOnlyModePermission.LOAD_INSECURE_ALLOW_SESSION);"
+ />
+ </hbox>
+ <separator class="thin" />
+ <listheader>
+ <treecol
+ id="siteCol"
+ data-l10n-id="permissions-site-name"
+ style="flex: 3 3 auto; width: 0"
+ onclick="gPermissionManager.buildPermissionsList(event.target)"
+ />
+ <treecol
+ id="statusCol"
+ data-l10n-id="permissions-status"
+ style="flex: 1 1 auto; width: 0"
+ data-isCurrentSortCol="true"
+ onclick="gPermissionManager.buildPermissionsList(event.target);"
+ />
+ </listheader>
+ <richlistbox
+ id="permissionsBox"
+ selected="false"
+ onkeypress="gPermissionManager.onPermissionKeyPress(event);"
+ onselect="gPermissionManager.onPermissionSelect();"
+ />
+ </vbox>
+
+ <hbox class="actionButtons">
+ <button
+ id="removePermission"
+ disabled="true"
+ data-l10n-id="permissions-remove"
+ oncommand="gPermissionManager.onPermissionDelete();"
+ />
+ <button
+ id="removeAllPermissions"
+ data-l10n-id="permissions-remove-all"
+ oncommand="gPermissionManager.onAllPermissionsDelete();"
+ />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/sanitize.js b/browser/components/preferences/dialogs/sanitize.js
new file mode 100644
index 0000000000..37f818e011
--- /dev/null
+++ b/browser/components/preferences/dialogs/sanitize.js
@@ -0,0 +1,38 @@
+/* -*- 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 */
+
+document
+ .querySelector("dialog")
+ .addEventListener("dialoghelp", window.top.openPrefsHelp);
+
+Preferences.addAll([
+ { id: "privacy.clearOnShutdown.history", type: "bool" },
+ { id: "privacy.clearOnShutdown.formdata", type: "bool" },
+ { id: "privacy.clearOnShutdown.downloads", type: "bool" },
+ { id: "privacy.clearOnShutdown.cookies", type: "bool" },
+ { id: "privacy.clearOnShutdown.cache", type: "bool" },
+ { id: "privacy.clearOnShutdown.offlineApps", type: "bool" },
+ { id: "privacy.clearOnShutdown.sessions", type: "bool" },
+ { id: "privacy.clearOnShutdown.siteSettings", type: "bool" },
+]);
+
+var gSanitizeDialog = Object.freeze({
+ init() {
+ this.onClearHistoryChanged();
+
+ Preferences.get("privacy.clearOnShutdown.history").on(
+ "change",
+ this.onClearHistoryChanged.bind(this)
+ );
+ },
+
+ onClearHistoryChanged() {
+ let downloadsPref = Preferences.get("privacy.clearOnShutdown.downloads");
+ let historyPref = Preferences.get("privacy.clearOnShutdown.history");
+ downloadsPref.value = historyPref.value;
+ },
+});
diff --git a/browser/components/preferences/dialogs/sanitize.xhtml b/browser/components/preferences/dialogs/sanitize.xhtml
new file mode 100644
index 0000000000..40f030d8ab
--- /dev/null
+++ b/browser/components/preferences/dialogs/sanitize.xhtml
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
+
+<!DOCTYPE window>
+
+<window
+ id="SanitizeDialog"
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="lastSelected"
+ data-l10n-id="sanitize-prefs2"
+ data-l10n-attrs="style"
+ onload="gSanitizeDialog.init();"
+>
+ <dialog buttons="accept,cancel,help" helpTopic="prefs-clear-private-data">
+ <linkset>
+ <html:link rel="localization" href="browser/sanitize.ftl" />
+ <html:link rel="localization" href="branding/brand.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="window-close"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <script src="chrome://browser/content/preferences/dialogs/sanitize.js" />
+
+ <description data-l10n-id="clear-data-settings-label"></description>
+
+ <groupbox>
+ <label><html:h2 data-l10n-id="history-section-label" /></label>
+ <hbox>
+ <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
+ <checkbox
+ data-l10n-id="item-history-and-downloads"
+ preference="privacy.clearOnShutdown.history"
+ />
+ <checkbox
+ data-l10n-id="item-active-logins"
+ preference="privacy.clearOnShutdown.sessions"
+ />
+ <checkbox
+ data-l10n-id="item-form-search-history"
+ preference="privacy.clearOnShutdown.formdata"
+ />
+ </vbox>
+ <vbox>
+ <checkbox
+ data-l10n-id="item-cookies"
+ preference="privacy.clearOnShutdown.cookies"
+ />
+ <checkbox
+ data-l10n-id="item-cache"
+ preference="privacy.clearOnShutdown.cache"
+ />
+ </vbox>
+ </hbox>
+ </groupbox>
+ <groupbox>
+ <label><html:h2 data-l10n-id="data-section-label" /></label>
+ <hbox>
+ <vbox data-l10n-id="sanitize-prefs-style" data-l10n-attrs="style">
+ <checkbox
+ data-l10n-id="item-site-settings"
+ preference="privacy.clearOnShutdown.siteSettings"
+ />
+ </vbox>
+ <vbox flex="1">
+ <checkbox
+ data-l10n-id="item-offline-apps"
+ preference="privacy.clearOnShutdown.offlineApps"
+ />
+ </vbox>
+ </hbox>
+ </groupbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/selectBookmark.js b/browser/components/preferences/dialogs/selectBookmark.js
new file mode 100644
index 0000000000..5fe4c645e7
--- /dev/null
+++ b/browser/components/preferences/dialogs/selectBookmark.js
@@ -0,0 +1,119 @@
+//* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/* Shared Places Import - change other consumers if you change this: */
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs",
+ PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ "PlacesTreeView",
+ "chrome://browser/content/places/treeView.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ this,
+ ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"],
+ "chrome://browser/content/places/controller.js"
+);
+/* End Shared Places Import */
+
+/**
+ * SelectBookmarkDialog controls the user interface for the "Use Bookmark for
+ * Home Page" dialog.
+ *
+ * The caller (gMainPane.setHomePageToBookmark in main.js) invokes this dialog
+ * with a single argument - a reference to an object with a .urls property and
+ * a .names property. This dialog is responsible for updating the contents of
+ * the .urls property with an array of URLs to use as home pages and for
+ * updating the .names property with an array of names for those URLs before it
+ * closes.
+ */
+var SelectBookmarkDialog = {
+ init: function SBD_init() {
+ document.getElementById("bookmarks").place =
+ "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY;
+
+ // Initial update of the OK button.
+ this.selectionChanged();
+ document.addEventListener("dialogaccept", function () {
+ SelectBookmarkDialog.accept();
+ });
+ },
+
+ /**
+ * Update the disabled state of the OK button as the user changes the
+ * selection within the view.
+ */
+ selectionChanged: function SBD_selectionChanged() {
+ var accept = document
+ .getElementById("selectBookmarkDialog")
+ .getButton("accept");
+ var bookmarks = document.getElementById("bookmarks");
+ var disableAcceptButton = true;
+ if (bookmarks.hasSelection) {
+ if (!PlacesUtils.nodeIsSeparator(bookmarks.selectedNode)) {
+ disableAcceptButton = false;
+ }
+ }
+ accept.disabled = disableAcceptButton;
+ },
+
+ onItemDblClick: function SBD_onItemDblClick() {
+ var bookmarks = document.getElementById("bookmarks");
+ var selectedNode = bookmarks.selectedNode;
+ if (selectedNode && PlacesUtils.nodeIsURI(selectedNode)) {
+ /**
+ * The user has double clicked on a tree row that is a link. Take this to
+ * mean that they want that link to be their homepage, and close the dialog.
+ */
+ document
+ .getElementById("selectBookmarkDialog")
+ .getButton("accept")
+ .click();
+ }
+ },
+
+ /**
+ * User accepts their selection. Set all the selected URLs or the contents
+ * of the selected folder as the list of homepages.
+ */
+ accept: function SBD_accept() {
+ var bookmarks = document.getElementById("bookmarks");
+ if (!bookmarks.hasSelection) {
+ throw new Error(
+ "Should not be able to accept dialog if there is no selected URL!"
+ );
+ }
+ var urls = [];
+ var names = [];
+ var selectedNode = bookmarks.selectedNode;
+ if (PlacesUtils.nodeIsFolder(selectedNode)) {
+ let concreteGuid = PlacesUtils.getConcreteItemGuid(selectedNode);
+ var contents = PlacesUtils.getFolderContents(concreteGuid).root;
+ var cc = contents.childCount;
+ for (var i = 0; i < cc; ++i) {
+ var node = contents.getChild(i);
+ if (PlacesUtils.nodeIsURI(node)) {
+ urls.push(node.uri);
+ names.push(node.title);
+ }
+ }
+ contents.containerOpen = false;
+ } else {
+ urls.push(selectedNode.uri);
+ names.push(selectedNode.title);
+ }
+ window.arguments[0].urls = urls;
+ window.arguments[0].names = names;
+ },
+};
diff --git a/browser/components/preferences/dialogs/selectBookmark.xhtml b/browser/components/preferences/dialogs/selectBookmark.xhtml
new file mode 100644
index 0000000000..3a46394328
--- /dev/null
+++ b/browser/components/preferences/dialogs/selectBookmark.xhtml
@@ -0,0 +1,55 @@
+<?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://browser/content/places/places.css"?>
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="select-bookmark-window2"
+ data-l10n-attrs="title, style"
+ persist="width height"
+ onload="SelectBookmarkDialog.init();"
+>
+ <dialog id="selectBookmarkDialog">
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/selectBookmark.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/selectBookmark.js" />
+ <script src="chrome://global/content/globalOverlay.js" />
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://browser/content/places/places-tree.js" />
+
+ <description data-l10n-id="select-bookmark-desc" />
+
+ <separator class="thin" />
+
+ <tree
+ id="bookmarks"
+ flex="1"
+ is="places-tree"
+ style="height: 15em"
+ hidecolumnpicker="true"
+ seltype="single"
+ ondblclick="SelectBookmarkDialog.onItemDblClick();"
+ onselect="SelectBookmarkDialog.selectionChanged();"
+ disableUserActions="true"
+ >
+ <treecols>
+ <treecol id="title" flex="1" primary="true" hideheader="true" />
+ </treecols>
+ <treechildren id="bookmarksChildren" flex="1" />
+ </tree>
+
+ <separator class="thin" />
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/siteDataRemoveSelected.js b/browser/components/preferences/dialogs/siteDataRemoveSelected.js
new file mode 100644
index 0000000000..e722a2d826
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataRemoveSelected.js
@@ -0,0 +1,56 @@
+/* -*- 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/. */
+
+"use strict";
+
+/**
+ * This dialog will ask the user to confirm that they really want to delete all
+ * site data for a number of hosts.
+ **/
+let gSiteDataRemoveSelected = {
+ init() {
+ document.addEventListener("dialogaccept", function () {
+ window.arguments[0].allowed = true;
+ });
+ document.addEventListener("dialogcancel", function () {
+ window.arguments[0].allowed = false;
+ });
+
+ let list = document.getElementById("removalList");
+
+ let hosts = window.arguments[0].hosts;
+
+ if (!hosts) {
+ throw new Error("Must specify hosts option in arguments.");
+ }
+ let dialog = document.getElementById("SiteDataRemoveSelectedDialog");
+ if (hosts.length == 1) {
+ dialog.classList.add("single-entry");
+ document.l10n.setAttributes(
+ document.getElementById("removing-description"),
+ "site-data-removing-single-desc",
+ {
+ baseDomain: hosts[0],
+ }
+ );
+ return;
+ }
+ dialog.classList.add("multi-entry");
+ hosts.sort();
+ let fragment = document.createDocumentFragment();
+ for (let host of hosts) {
+ let listItem = document.createXULElement("richlistitem");
+ let label = document.createXULElement("label");
+ if (host) {
+ label.setAttribute("value", host);
+ } else {
+ document.l10n.setAttributes(label, "site-data-local-file-host");
+ }
+ listItem.appendChild(label);
+ fragment.appendChild(listItem);
+ }
+ list.appendChild(fragment);
+ },
+};
diff --git a/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml b/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml
new file mode 100644
index 0000000000..56b50f3d53
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataRemoveSelected.xhtml
@@ -0,0 +1,48 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/siteDataSettings.css" type="text/css"?>
+
+<window id="SiteDataRemoveSelectedDialog"
+ width="500"
+ data-l10n-id="site-data-removing-dialog"
+ data-l10n-attrs="title"
+ onload="gSiteDataRemoveSelected.init();"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<dialog data-l10n-id="site-data-removing-dialog"
+ data-l10n-attrs="buttonlabelaccept">
+
+ <linkset>
+ <html:link rel="localization" href="browser/preferences/siteDataSettings.ftl"/>
+ </linkset>
+
+ <hbox>
+ <vbox>
+ <image class="question-icon"/>
+ </vbox>
+ <vbox flex="1">
+ <!-- Only show this label on OS X because of no dialog title -->
+ <label id="removing-label"
+ data-l10n-id="site-data-removing-header"
+#ifndef XP_MACOSX
+ hidden="true"
+#endif
+ />
+ <separator class="thin"/>
+ <description id="removing-description" data-l10n-id="site-data-removing-desc"/>
+ </vbox>
+ </hbox>
+ <separator class="multi-site"/>
+
+ <label data-l10n-id="site-data-removing-table" class="multi-site"/>
+ <separator class="thin multi-site"/>
+ <richlistbox id="removalList" class="theme-listbox multi-site" flex="1"/>
+ <!-- Load the script after the elements for layout issues (bug 1501755). -->
+ <script src="chrome://browser/content/preferences/dialogs/siteDataRemoveSelected.js"/>
+</dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/siteDataSettings.js b/browser/components/preferences/dialogs/siteDataSettings.js
new file mode 100644
index 0000000000..7c0c490e5d
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataSettings.js
@@ -0,0 +1,335 @@
+/* -*- 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/. */
+
+"use strict";
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "SiteDataManager",
+ "resource:///modules/SiteDataManager.jsm"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+});
+
+let gSiteDataSettings = {
+ // Array of metadata of sites. Each array element is object holding:
+ // - uri: uri of site; instance of nsIURI
+ // - baseDomain: base domain of the site
+ // - cookies: array of cookies of that site
+ // - usage: disk usage which site uses
+ // - userAction: "remove" or "update-permission"; the action user wants to take.
+ _sites: null,
+
+ _list: null,
+ _searchBox: null,
+
+ _createSiteListItem(site) {
+ let item = document.createXULElement("richlistitem");
+ item.setAttribute("host", site.baseDomain);
+ let container = document.createXULElement("hbox");
+
+ // Creates a new column item with the specified relative width.
+ function addColumnItem(l10n, flexWidth, tooltipText) {
+ let box = document.createXULElement("hbox");
+ box.className = "item-box";
+ box.setAttribute("style", `flex: ${flexWidth} ${flexWidth};`);
+ let label = document.createXULElement("label");
+ label.setAttribute("crop", "end");
+ if (l10n) {
+ if (l10n.hasOwnProperty("raw")) {
+ box.setAttribute("tooltiptext", l10n.raw);
+ label.setAttribute("value", l10n.raw);
+ } else {
+ document.l10n.setAttributes(label, l10n.id, l10n.args);
+ }
+ }
+ if (tooltipText) {
+ box.setAttribute("tooltiptext", tooltipText);
+ }
+ box.appendChild(label);
+ container.appendChild(box);
+ }
+
+ // Add "Host" column.
+ let hostData = site.baseDomain
+ ? { raw: site.baseDomain }
+ : { id: "site-data-local-file-host" };
+ addColumnItem(hostData, "4");
+
+ // Add "Cookies" column.
+ addColumnItem({ raw: site.cookies.length }, "1");
+
+ // Add "Storage" column
+ if (site.usage > 0 || site.persisted) {
+ let [value, unit] = DownloadUtils.convertByteUnits(site.usage);
+ let strName = site.persisted
+ ? "site-storage-persistent"
+ : "site-storage-usage";
+ addColumnItem(
+ {
+ id: strName,
+ args: { value, unit },
+ },
+ "2"
+ );
+ } else {
+ // Pass null to avoid showing "0KB" when there is no site data stored.
+ addColumnItem(null, "2");
+ }
+
+ // Add "Last Used" column.
+ let formattedLastAccessed =
+ site.lastAccessed > 0
+ ? this._relativeTimeFormat.formatBestUnit(site.lastAccessed)
+ : null;
+ let formattedFullDate =
+ site.lastAccessed > 0
+ ? this._absoluteTimeFormat.format(site.lastAccessed)
+ : null;
+ addColumnItem(
+ site.lastAccessed > 0 ? { raw: formattedLastAccessed } : null,
+ "2",
+ formattedFullDate
+ );
+
+ item.appendChild(container);
+ return item;
+ },
+
+ init() {
+ function setEventListener(id, eventType, callback) {
+ document
+ .getElementById(id)
+ .addEventListener(eventType, callback.bind(gSiteDataSettings));
+ }
+
+ this._absoluteTimeFormat = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+ timeStyle: "short",
+ });
+
+ this._relativeTimeFormat = new Services.intl.RelativeTimeFormat(
+ undefined,
+ {}
+ );
+
+ this._list = document.getElementById("sitesList");
+ this._searchBox = document.getElementById("searchBox");
+ SiteDataManager.getSites().then(sites => {
+ this._sites = sites;
+ let sortCol = document.querySelector(
+ "treecol[data-isCurrentSortCol=true]"
+ );
+ this._sortSites(this._sites, sortCol);
+ this._buildSitesList(this._sites);
+ Services.obs.notifyObservers(null, "sitedata-settings-init");
+ });
+
+ setEventListener("sitesList", "select", this.onSelect);
+ setEventListener("hostCol", "click", this.onClickTreeCol);
+ setEventListener("usageCol", "click", this.onClickTreeCol);
+ setEventListener("lastAccessedCol", "click", this.onClickTreeCol);
+ setEventListener("cookiesCol", "click", this.onClickTreeCol);
+ setEventListener("searchBox", "command", this.onCommandSearch);
+ setEventListener("removeAll", "command", this.onClickRemoveAll);
+ setEventListener("removeSelected", "command", this.removeSelected);
+
+ document.addEventListener("dialogaccept", e => this.saveChanges(e));
+ },
+
+ _updateButtonsState() {
+ let items = this._list.getElementsByTagName("richlistitem");
+ let removeSelectedBtn = document.getElementById("removeSelected");
+ let removeAllBtn = document.getElementById("removeAll");
+ removeSelectedBtn.disabled = !this._list.selectedItems.length;
+ removeAllBtn.disabled = !items.length;
+
+ let l10nId = this._searchBox.value
+ ? "site-data-remove-shown"
+ : "site-data-remove-all";
+ document.l10n.setAttributes(removeAllBtn, l10nId);
+ },
+
+ /**
+ * @param sites {Array}
+ * @param col {XULElement} the <treecol> being sorted on
+ */
+ _sortSites(sites, col) {
+ let isCurrentSortCol = col.getAttribute("data-isCurrentSortCol");
+ let sortDirection =
+ col.getAttribute("data-last-sortDirection") || "ascending";
+ if (isCurrentSortCol) {
+ // Sort on the current column, flip the sorting direction
+ sortDirection =
+ sortDirection === "ascending" ? "descending" : "ascending";
+ }
+
+ let sortFunc = null;
+ switch (col.id) {
+ case "hostCol":
+ sortFunc = (a, b) => {
+ let aHost = a.baseDomain.toLowerCase();
+ let bHost = b.baseDomain.toLowerCase();
+ return aHost.localeCompare(bHost);
+ };
+ break;
+
+ case "cookiesCol":
+ sortFunc = (a, b) => a.cookies.length - b.cookies.length;
+ break;
+
+ case "usageCol":
+ sortFunc = (a, b) => a.usage - b.usage;
+ break;
+
+ case "lastAccessedCol":
+ sortFunc = (a, b) => a.lastAccessed - b.lastAccessed;
+ break;
+ }
+ if (sortDirection === "descending") {
+ sites.sort((a, b) => sortFunc(b, a));
+ } else {
+ sites.sort(sortFunc);
+ }
+
+ let cols = this._list.previousElementSibling.querySelectorAll("treecol");
+ cols.forEach(c => {
+ c.removeAttribute("sortDirection");
+ c.removeAttribute("data-isCurrentSortCol");
+ });
+ col.setAttribute("data-isCurrentSortCol", true);
+ col.setAttribute("sortDirection", sortDirection);
+ col.setAttribute("data-last-sortDirection", sortDirection);
+ },
+
+ /**
+ * @param sites {Array} array of metadata of sites
+ */
+ _buildSitesList(sites) {
+ // Clear old entries.
+ let oldItems = this._list.querySelectorAll("richlistitem");
+ for (let item of oldItems) {
+ item.remove();
+ }
+
+ let keyword = this._searchBox.value.toLowerCase().trim();
+ let fragment = document.createDocumentFragment();
+ for (let site of sites) {
+ if (keyword && !site.baseDomain.includes(keyword)) {
+ continue;
+ }
+
+ if (site.userAction === "remove") {
+ continue;
+ }
+
+ let item = this._createSiteListItem(site);
+ fragment.appendChild(item);
+ }
+ this._list.appendChild(fragment);
+ this._updateButtonsState();
+ },
+
+ _removeSiteItems(items) {
+ for (let i = items.length - 1; i >= 0; --i) {
+ let item = items[i];
+ let baseDomain = item.getAttribute("host");
+ let siteForBaseDomain = this._sites.find(
+ site => site.baseDomain == baseDomain
+ );
+ if (siteForBaseDomain) {
+ siteForBaseDomain.userAction = "remove";
+ }
+ item.remove();
+ }
+ this._updateButtonsState();
+ },
+
+ async saveChanges(event) {
+ let removals = this._sites
+ .filter(site => site.userAction == "remove")
+ .map(site => site.baseDomain);
+
+ if (removals.length) {
+ let removeAll = removals.length == this._sites.length;
+ let promptArg = removeAll ? undefined : removals;
+ if (!SiteDataManager.promptSiteDataRemoval(window, promptArg)) {
+ // If the user cancelled the confirm dialog keep the site data window open,
+ // they can still press cancel again to exit.
+ event.preventDefault();
+ return;
+ }
+ try {
+ if (removeAll) {
+ await SiteDataManager.removeAll();
+ } else {
+ await SiteDataManager.remove(removals);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ },
+
+ removeSelected() {
+ let lastIndex = this._list.selectedItems.length - 1;
+ let lastSelectedItem = this._list.selectedItems[lastIndex];
+ let lastSelectedItemPosition = this._list.getIndexOfItem(lastSelectedItem);
+ let nextSelectedItem = this._list.getItemAtIndex(
+ lastSelectedItemPosition + 1
+ );
+
+ this._removeSiteItems(this._list.selectedItems);
+ this._list.clearSelection();
+
+ if (nextSelectedItem) {
+ this._list.selectedItem = nextSelectedItem;
+ } else {
+ this._list.selectedIndex = this._list.itemCount - 1;
+ }
+ },
+
+ onClickTreeCol(e) {
+ this._sortSites(this._sites, e.target);
+ this._buildSitesList(this._sites);
+ this._list.clearSelection();
+ },
+
+ onCommandSearch() {
+ this._buildSitesList(this._sites);
+ this._list.clearSelection();
+ },
+
+ onClickRemoveAll() {
+ let siteItems = this._list.getElementsByTagName("richlistitem");
+ if (siteItems.length) {
+ this._removeSiteItems(siteItems);
+ }
+ },
+
+ onKeyPress(e) {
+ if (
+ e.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ e.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+ ) {
+ if (!e.target.closest("#sitesList")) {
+ // The user is typing or has not selected an item from the list to remove
+ return;
+ }
+ // The users intention is to delete site data
+ this.removeSelected();
+ }
+ },
+
+ onSelect() {
+ this._updateButtonsState();
+ },
+};
diff --git a/browser/components/preferences/dialogs/siteDataSettings.xhtml b/browser/components/preferences/dialogs/siteDataSettings.xhtml
new file mode 100644
index 0000000000..251b70d3fe
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataSettings.xhtml
@@ -0,0 +1,86 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/siteDataSettings.css" type="text/css"?>
+
+<window
+ id="SiteDataSettingsDialog"
+ data-l10n-id="site-data-settings-window"
+ data-l10n-attrs="title"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="min-width: 45em"
+ onload="gSiteDataSettings.init();"
+ onkeypress="gSiteDataSettings.onKeyPress(event);"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="site-data-settings-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="browser/preferences/siteDataSettings.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/siteDataSettings.js" />
+
+ <vbox flex="1" class="contentPane">
+ <description
+ id="settingsDescription"
+ data-l10n-id="site-data-settings-description"
+ />
+ <separator class="thin" />
+
+ <hbox id="searchBoxContainer">
+ <search-textbox
+ id="searchBox"
+ flex="1"
+ data-l10n-id="site-data-search-textbox"
+ data-l10n-attrs="placeholder"
+ />
+ </hbox>
+ <separator class="thin" />
+
+ <listheader>
+ <treecol
+ style="flex: 4 4 auto; width: 50px"
+ data-l10n-id="site-data-column-host"
+ id="hostCol"
+ />
+ <treecol
+ style="flex: 1 auto; width: 50px"
+ data-l10n-id="site-data-column-cookies"
+ id="cookiesCol"
+ />
+ <!-- Sorted by usage so the user can quickly see which sites use the most data. -->
+ <treecol
+ style="flex: 2 2 auto; width: 50px"
+ data-l10n-id="site-data-column-storage"
+ id="usageCol"
+ data-isCurrentSortCol="true"
+ />
+ <treecol
+ style="flex: 2 2 auto; width: 50px"
+ data-l10n-id="site-data-column-last-used"
+ id="lastAccessedCol"
+ />
+ </listheader>
+ <richlistbox seltype="multiple" id="sitesList" orient="vertical" />
+ </vbox>
+
+ <hbox align="start">
+ <button id="removeSelected" data-l10n-id="site-data-remove-selected" />
+ <button id="removeAll" />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/sitePermissions.css b/browser/components/preferences/dialogs/sitePermissions.css
new file mode 100644
index 0000000000..e41c7203fe
--- /dev/null
+++ b/browser/components/preferences/dialogs/sitePermissions.css
@@ -0,0 +1,70 @@
+/* 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/. */
+
+.website-name,
+hbox.website-status {
+ overflow: hidden; /* Allows equal sizing combined with width="0" */
+ padding-inline-start: 7px;
+ align-items: center;
+}
+
+#permissionsBox {
+ flex: 1 auto;
+ height: 18em;
+ min-height: 70px; /* 2 * 35px, which is the min row height specified below */
+}
+
+#siteCol,
+#statusCol,
+#permissionsBox > richlistitem {
+ min-height: 35px;
+}
+
+#permissionsBox > richlistitem > hbox {
+ flex: 1;
+}
+
+#siteCol {
+ flex: 3 3 auto;
+}
+
+.website-name {
+ flex: 3 3;
+}
+
+#statusCol {
+ flex: 1 auto;
+}
+
+.website-status {
+ flex: 1;
+}
+
+/* TODO(bug 1802993): Seems this could be on .website-name instead of label? */
+#siteCol,
+#statusCol,
+.website-name > label,
+.website-status {
+ width: 75px;
+}
+
+menulist.website-status {
+ margin: 1px;
+ margin-inline-end: 5px;
+}
+
+#browserNotificationsPermissionExtensionContent,
+#permissionsDisableDescription {
+ margin-inline-start: 32px;
+}
+
+#permissionsDisableDescription {
+ color: var(--text-color-deemphasized);
+ line-height: 110%;
+}
+
+#permissionsDisableCheckbox {
+ margin-inline-start: 4px;
+ padding-top: 10px;
+}
diff --git a/browser/components/preferences/dialogs/sitePermissions.js b/browser/components/preferences/dialogs/sitePermissions.js
new file mode 100644
index 0000000000..61af43463b
--- /dev/null
+++ b/browser/components/preferences/dialogs/sitePermissions.js
@@ -0,0 +1,679 @@
+/* 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 */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { SitePermissions } = ChromeUtils.importESModule(
+ "resource:///modules/SitePermissions.sys.mjs"
+);
+
+const sitePermissionsL10n = {
+ "desktop-notification": {
+ window: "permissions-site-notification-window2",
+ description: "permissions-site-notification-desc",
+ disableLabel: "permissions-site-notification-disable-label",
+ disableDescription: "permissions-site-notification-disable-desc",
+ },
+ geo: {
+ window: "permissions-site-location-window2",
+ description: "permissions-site-location-desc",
+ disableLabel: "permissions-site-location-disable-label",
+ disableDescription: "permissions-site-location-disable-desc",
+ },
+ xr: {
+ window: "permissions-site-xr-window2",
+ description: "permissions-site-xr-desc",
+ disableLabel: "permissions-site-xr-disable-label",
+ disableDescription: "permissions-site-xr-disable-desc",
+ },
+ camera: {
+ window: "permissions-site-camera-window2",
+ description: "permissions-site-camera-desc",
+ disableLabel: "permissions-site-camera-disable-label",
+ disableDescription: "permissions-site-camera-disable-desc",
+ },
+ microphone: {
+ window: "permissions-site-microphone-window2",
+ description: "permissions-site-microphone-desc",
+ disableLabel: "permissions-site-microphone-disable-label",
+ disableDescription: "permissions-site-microphone-disable-desc",
+ },
+ speaker: {
+ window: "permissions-site-speaker-window",
+ description: "permissions-site-speaker-desc",
+ },
+ "autoplay-media": {
+ window: "permissions-site-autoplay-window2",
+ description: "permissions-site-autoplay-desc",
+ },
+};
+
+const sitePermissionsConfig = {
+ "autoplay-media": {
+ _getCapabilityString(capability) {
+ switch (capability) {
+ case SitePermissions.ALLOW:
+ return "permissions-capabilities-autoplay-allow";
+ case SitePermissions.BLOCK:
+ return "permissions-capabilities-autoplay-block";
+ case SitePermissions.AUTOPLAY_BLOCKED_ALL:
+ return "permissions-capabilities-autoplay-blockall";
+ }
+ throw new Error(`Unknown capability: ${capability}`);
+ },
+ },
+};
+
+// A set of permissions for a single origin. One PermissionGroup instance
+// corresponds to one row in the gSitePermissionsManager._list richlistbox.
+// Permissions may be single or double keyed, but the primary key of all
+// permissions matches the permission type of the dialog.
+class PermissionGroup {
+ #changedCapability;
+
+ constructor(perm) {
+ this.principal = perm.principal;
+ this.origin = perm.principal.origin;
+ this.perms = [perm];
+ }
+ addPermission(perm) {
+ this.perms.push(perm);
+ }
+ removePermission(perm) {
+ this.perms = this.perms.filter(p => p.type != perm.type);
+ }
+ set capability(cap) {
+ this.#changedCapability = cap;
+ }
+ get capability() {
+ if (this.#changedCapability) {
+ return this.#changedCapability;
+ }
+ return this.savedCapability;
+ }
+ revert() {
+ this.#changedCapability = null;
+ }
+ get savedCapability() {
+ // This logic to present a single capability for permissions of different
+ // keys and capabilities caters for speaker-selection, where a block
+ // permission may be set for all devices with no second key, which would
+ // override any device-specific double-keyed allow permissions.
+ let cap;
+ for (let perm of this.perms) {
+ let [type] = perm.type.split(SitePermissions.PERM_KEY_DELIMITER);
+ if (type == perm.type) {
+ // No second key. This overrides double-keyed perms.
+ return perm.capability;
+ }
+ // Double-keyed perms are not expected to have different capabilities.
+ cap = perm.capability;
+ }
+ return cap;
+ }
+}
+
+const PERMISSION_STATES = [
+ SitePermissions.ALLOW,
+ SitePermissions.BLOCK,
+ SitePermissions.PROMPT,
+ SitePermissions.AUTOPLAY_BLOCKED_ALL,
+];
+
+const NOTIFICATIONS_PERMISSION_OVERRIDE_KEY = "webNotificationsDisabled";
+const NOTIFICATIONS_PERMISSION_PREF =
+ "permissions.default.desktop-notification";
+
+const AUTOPLAY_PREF = "media.autoplay.default";
+
+var gSitePermissionsManager = {
+ _type: "",
+ _isObserving: false,
+ _permissionGroups: new Map(),
+ _permissionsToChange: new Map(),
+ _permissionsToDelete: new Map(),
+ _list: null,
+ _removeButton: null,
+ _removeAllButton: null,
+ _searchBox: null,
+ _checkbox: null,
+ _currentDefaultPermissionsState: null,
+ _defaultPermissionStatePrefName: null,
+
+ onLoad() {
+ let params = window.arguments[0];
+ document.mozSubdialogReady = this.init(params);
+ },
+
+ async init(params) {
+ if (!this._isObserving) {
+ Services.obs.addObserver(this, "perm-changed");
+ this._isObserving = true;
+ }
+
+ document.addEventListener("dialogaccept", () => this.onApplyChanges());
+
+ this._type = params.permissionType;
+ this._list = document.getElementById("permissionsBox");
+ this._removeButton = document.getElementById("removePermission");
+ this._removeAllButton = document.getElementById("removeAllPermissions");
+ this._searchBox = document.getElementById("searchBox");
+ this._checkbox = document.getElementById("permissionsDisableCheckbox");
+ this._disableExtensionButton = document.getElementById(
+ "disableNotificationsPermissionExtension"
+ );
+ this._permissionsDisableDescription = document.getElementById(
+ "permissionsDisableDescription"
+ );
+ this._setAutoplayPref = document.getElementById("setAutoplayPref");
+
+ let permissionsText = document.getElementById("permissionsText");
+
+ document.l10n.pauseObserving();
+ let l10n = sitePermissionsL10n[this._type];
+ document.l10n.setAttributes(permissionsText, l10n.description);
+ if (l10n.disableLabel) {
+ document.l10n.setAttributes(this._checkbox, l10n.disableLabel);
+ }
+ if (l10n.disableDescription) {
+ document.l10n.setAttributes(
+ this._permissionsDisableDescription,
+ l10n.disableDescription
+ );
+ }
+ document.l10n.setAttributes(document.documentElement, l10n.window);
+
+ await document.l10n.translateElements([
+ permissionsText,
+ this._checkbox,
+ this._permissionsDisableDescription,
+ document.documentElement,
+ ]);
+ document.l10n.resumeObserving();
+
+ // Initialize the checkbox state and handle showing notification permission UI
+ // when it is disabled by an extension.
+ this._defaultPermissionStatePrefName = "permissions.default." + this._type;
+ this._watchPermissionPrefChange();
+
+ this._loadPermissions();
+ this.buildPermissionsList();
+
+ if (params.permissionType == "autoplay-media") {
+ await this.buildAutoplayMenulist();
+ this._setAutoplayPref.hidden = false;
+ }
+
+ this._searchBox.focus();
+ },
+
+ uninit() {
+ if (this._isObserving) {
+ Services.obs.removeObserver(this, "perm-changed");
+ this._isObserving = false;
+ }
+ if (this._setAutoplayPref) {
+ this._setAutoplayPref.hidden = true;
+ }
+ },
+
+ observe(subject, topic, data) {
+ if (topic !== "perm-changed") {
+ return;
+ }
+
+ let permission = subject.QueryInterface(Ci.nsIPermission);
+ let [type] = permission.type.split(SitePermissions.PERM_KEY_DELIMITER);
+
+ // Ignore unrelated permission types and permissions with unknown states.
+ if (
+ type !== this._type ||
+ !PERMISSION_STATES.includes(permission.capability)
+ ) {
+ return;
+ }
+
+ if (data == "added") {
+ this._addPermissionToList(permission);
+ } else {
+ let group = this._permissionGroups.get(permission.principal.origin);
+ if (!group) {
+ // already moved to _permissionsToDelete
+ // or private browsing session permission
+ return;
+ }
+ if (data == "changed") {
+ group.removePermission(permission);
+ group.addPermission(permission);
+ } else if (data == "deleted") {
+ group.removePermission(permission);
+ if (!group.perms.length) {
+ this._removePermissionFromList(permission.principal.origin);
+ return;
+ }
+ }
+ }
+ this.buildPermissionsList();
+ },
+
+ _handleCheckboxUIUpdates() {
+ let pref = Services.prefs.getPrefType(this._defaultPermissionStatePrefName);
+ if (pref != Services.prefs.PREF_INVALID) {
+ this._currentDefaultPermissionsState = Services.prefs.getIntPref(
+ this._defaultPermissionStatePrefName
+ );
+ }
+
+ if (this._currentDefaultPermissionsState === null) {
+ this._checkbox.hidden = true;
+ this._permissionsDisableDescription.hidden = true;
+ } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
+ this._checkbox.checked = true;
+ } else {
+ this._checkbox.checked = false;
+ }
+
+ if (Services.prefs.prefIsLocked(this._defaultPermissionStatePrefName)) {
+ this._checkbox.disabled = true;
+ }
+ },
+
+ /**
+ * Listen for changes to the permissions.default.* pref and make
+ * necessary changes to the UI.
+ */
+ _watchPermissionPrefChange() {
+ this._handleCheckboxUIUpdates();
+
+ if (this._type == "desktop-notification") {
+ this._handleWebNotificationsDisable();
+
+ this._disableExtensionButton.addEventListener(
+ "command",
+ makeDisableControllingExtension(
+ PREF_SETTING_TYPE,
+ NOTIFICATIONS_PERMISSION_OVERRIDE_KEY
+ )
+ );
+ }
+
+ let observer = () => {
+ this._handleCheckboxUIUpdates();
+ if (this._type == "desktop-notification") {
+ this._handleWebNotificationsDisable();
+ }
+ };
+ Services.prefs.addObserver(this._defaultPermissionStatePrefName, observer);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver(
+ this._defaultPermissionStatePrefName,
+ observer
+ );
+ });
+ },
+
+ /**
+ * Handles the UI update for web notifications disable by extensions.
+ */
+ async _handleWebNotificationsDisable() {
+ let prefLocked = Services.prefs.prefIsLocked(NOTIFICATIONS_PERMISSION_PREF);
+ if (prefLocked) {
+ // An extension can't control these settings if they're locked.
+ hideControllingExtension(NOTIFICATIONS_PERMISSION_OVERRIDE_KEY);
+ } else {
+ let isControlled = await handleControllingExtension(
+ PREF_SETTING_TYPE,
+ NOTIFICATIONS_PERMISSION_OVERRIDE_KEY
+ );
+ this._checkbox.disabled = isControlled;
+ }
+ },
+
+ _getCapabilityL10nId(element, type, capability) {
+ if (
+ type in sitePermissionsConfig &&
+ sitePermissionsConfig[type]._getCapabilityString
+ ) {
+ return sitePermissionsConfig[type]._getCapabilityString(capability);
+ }
+ switch (element.tagName) {
+ case "menuitem":
+ switch (capability) {
+ case Services.perms.ALLOW_ACTION:
+ return "permissions-capabilities-allow";
+ case Services.perms.DENY_ACTION:
+ return "permissions-capabilities-block";
+ case Services.perms.PROMPT_ACTION:
+ return "permissions-capabilities-prompt";
+ default:
+ throw new Error(`Unknown capability: ${capability}`);
+ }
+ case "label":
+ switch (capability) {
+ case Services.perms.ALLOW_ACTION:
+ return "permissions-capabilities-listitem-allow";
+ case Services.perms.DENY_ACTION:
+ return "permissions-capabilities-listitem-block";
+ default:
+ throw new Error(`Unexpected capability: ${capability}`);
+ }
+ default:
+ throw new Error(`Unexpected tag: ${element.tagName}`);
+ }
+ },
+
+ _addPermissionToList(perm) {
+ let [type] = perm.type.split(SitePermissions.PERM_KEY_DELIMITER);
+ // Ignore unrelated permission types and permissions with unknown states.
+ if (
+ type !== this._type ||
+ !PERMISSION_STATES.includes(perm.capability) ||
+ // Skip private browsing session permissions
+ (perm.principal.privateBrowsingId !==
+ Services.scriptSecurityManager.DEFAULT_PRIVATE_BROWSING_ID &&
+ perm.expireType === Services.perms.EXPIRE_SESSION)
+ ) {
+ return;
+ }
+ let group = this._permissionGroups.get(perm.principal.origin);
+ if (group) {
+ group.addPermission(perm);
+ } else {
+ group = new PermissionGroup(perm);
+ this._permissionGroups.set(group.origin, group);
+ }
+ },
+
+ _removePermissionFromList(origin) {
+ this._permissionGroups.delete(origin);
+ this._permissionsToChange.delete(origin);
+ let permissionlistitem = document.getElementsByAttribute(
+ "origin",
+ origin
+ )[0];
+ if (permissionlistitem) {
+ permissionlistitem.remove();
+ }
+ },
+
+ _loadPermissions() {
+ // load permissions into a table.
+ for (let nextPermission of Services.perms.all) {
+ this._addPermissionToList(nextPermission);
+ }
+ },
+
+ _createPermissionListItem(permissionGroup) {
+ let richlistitem = document.createXULElement("richlistitem");
+ richlistitem.setAttribute("origin", permissionGroup.origin);
+ let row = document.createXULElement("hbox");
+
+ let hbox = document.createXULElement("hbox");
+ let website = document.createXULElement("label");
+ website.setAttribute("value", permissionGroup.origin);
+ hbox.setAttribute("class", "website-name");
+ hbox.appendChild(website);
+
+ let states = SitePermissions.getAvailableStates(this._type).filter(
+ state => state != SitePermissions.UNKNOWN
+ );
+ // Handle the cases of a double-keyed ALLOW permission or a PROMPT
+ // permission after the default has been changed back to UNKNOWN.
+ if (!states.includes(permissionGroup.savedCapability)) {
+ states.unshift(permissionGroup.savedCapability);
+ }
+ let siteStatus;
+ if (states.length == 1) {
+ // Only a single state is available. Show a label.
+ siteStatus = document.createXULElement("hbox");
+ let label = document.createXULElement("label");
+ siteStatus.appendChild(label);
+ document.l10n.setAttributes(
+ label,
+ this._getCapabilityL10nId(label, this._type, permissionGroup.capability)
+ );
+ } else {
+ // Multiple states are available. Show a menulist.
+ siteStatus = document.createXULElement("menulist");
+ for (let state of states) {
+ let m = siteStatus.appendItem(undefined, state);
+ document.l10n.setAttributes(
+ m,
+ this._getCapabilityL10nId(m, this._type, state)
+ );
+ }
+ siteStatus.addEventListener("select", () => {
+ this.onPermissionChange(permissionGroup, Number(siteStatus.value));
+ });
+ }
+ siteStatus.setAttribute("class", "website-status");
+ siteStatus.value = permissionGroup.capability;
+
+ row.appendChild(hbox);
+ row.appendChild(siteStatus);
+ richlistitem.appendChild(row);
+ return richlistitem;
+ },
+
+ onPermissionKeyPress(event) {
+ if (!this._list.selectedItem) {
+ return;
+ }
+
+ if (
+ event.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ event.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+ ) {
+ this.onPermissionDelete();
+ event.preventDefault();
+ }
+ },
+
+ _setRemoveButtonState() {
+ if (!this._list) {
+ return;
+ }
+
+ let hasSelection = this._list.selectedIndex >= 0;
+ let hasRows = this._list.itemCount > 0;
+ this._removeButton.disabled = !hasSelection;
+ this._removeAllButton.disabled = !hasRows;
+ },
+
+ onPermissionDelete() {
+ let richlistitem = this._list.selectedItem;
+ let origin = richlistitem.getAttribute("origin");
+ let permissionGroup = this._permissionGroups.get(origin);
+
+ this._removePermissionFromList(origin);
+ this._permissionsToDelete.set(permissionGroup.origin, permissionGroup);
+
+ this._setRemoveButtonState();
+ },
+
+ onAllPermissionsDelete() {
+ for (let permissionGroup of this._permissionGroups.values()) {
+ this._removePermissionFromList(permissionGroup.origin);
+ this._permissionsToDelete.set(permissionGroup.origin, permissionGroup);
+ }
+
+ this._setRemoveButtonState();
+ },
+
+ onPermissionSelect() {
+ this._setRemoveButtonState();
+ },
+
+ onPermissionChange(perm, capability) {
+ let group = this._permissionGroups.get(perm.origin);
+ if (group.capability == capability) {
+ return;
+ }
+ if (capability == group.savedCapability) {
+ group.revert();
+ this._permissionsToChange.delete(group.origin);
+ } else {
+ group.capability = capability;
+ this._permissionsToChange.set(group.origin, group);
+ }
+
+ // enable "remove all" button as needed
+ this._setRemoveButtonState();
+ },
+
+ 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();
+
+ // Delete even _permissionsToChange to clear out double-keyed permissions
+ for (let group of [
+ ...this._permissionsToDelete.values(),
+ ...this._permissionsToChange.values(),
+ ]) {
+ for (let perm of group.perms) {
+ SitePermissions.removeFromPrincipal(perm.principal, perm.type);
+ }
+ }
+
+ for (let group of this._permissionsToChange.values()) {
+ SitePermissions.setForPrincipal(
+ group.principal,
+ this._type,
+ group.capability
+ );
+ }
+
+ if (this._checkbox.checked) {
+ Services.prefs.setIntPref(
+ this._defaultPermissionStatePrefName,
+ SitePermissions.BLOCK
+ );
+ } else if (this._currentDefaultPermissionsState == SitePermissions.BLOCK) {
+ Services.prefs.setIntPref(
+ this._defaultPermissionStatePrefName,
+ SitePermissions.UNKNOWN
+ );
+ }
+ },
+
+ buildPermissionsList(sortCol) {
+ // Clear old entries.
+ let oldItems = this._list.querySelectorAll("richlistitem");
+ for (let item of oldItems) {
+ item.remove();
+ }
+ let frag = document.createDocumentFragment();
+
+ let permissionGroups = Array.from(this._permissionGroups.values());
+
+ let keyword = this._searchBox.value.toLowerCase().trim();
+ for (let permissionGroup of permissionGroups) {
+ if (keyword && !permissionGroup.origin.includes(keyword)) {
+ continue;
+ }
+
+ let richlistitem = this._createPermissionListItem(permissionGroup);
+ frag.appendChild(richlistitem);
+ }
+
+ // Sort permissions.
+ this._sortPermissions(this._list, frag, sortCol);
+
+ this._list.appendChild(frag);
+
+ this._setRemoveButtonState();
+ },
+
+ async buildAutoplayMenulist() {
+ let menulist = document.createXULElement("menulist");
+ let states = SitePermissions.getAvailableStates("autoplay-media");
+ document.l10n.pauseObserving();
+ for (let state of states) {
+ let m = menulist.appendItem(undefined, state);
+ document.l10n.setAttributes(
+ m,
+ this._getCapabilityL10nId(m, "autoplay-media", state)
+ );
+ }
+
+ menulist.value = SitePermissions.getDefault("autoplay-media");
+
+ menulist.addEventListener("select", () => {
+ SitePermissions.setDefault("autoplay-media", Number(menulist.value));
+ });
+
+ menulist.menupopup.setAttribute("incontentshell", "false");
+
+ menulist.disabled = Services.prefs.prefIsLocked(AUTOPLAY_PREF);
+
+ document.getElementById("setAutoplayPref").appendChild(menulist);
+ await document.l10n.translateFragment(menulist);
+ document.l10n.resumeObserving();
+ },
+
+ _sortPermissions(list, frag, column) {
+ let sortDirection;
+
+ if (!column) {
+ column = document.querySelector("treecol[data-isCurrentSortCol=true]");
+ sortDirection =
+ column.getAttribute("data-last-sortDirection") || "ascending";
+ } else {
+ sortDirection = column.getAttribute("data-last-sortDirection");
+ sortDirection =
+ sortDirection === "ascending" ? "descending" : "ascending";
+ }
+
+ let sortFunc = null;
+ switch (column.id) {
+ case "siteCol":
+ sortFunc = (a, b) => {
+ return comp.compare(
+ a.getAttribute("origin"),
+ b.getAttribute("origin")
+ );
+ };
+ break;
+
+ case "statusCol":
+ sortFunc = (a, b) => {
+ return (
+ parseInt(a.querySelector(".website-status").value) >
+ parseInt(b.querySelector(".website-status").value)
+ );
+ };
+ break;
+ }
+
+ let comp = new Services.intl.Collator(undefined, {
+ usage: "sort",
+ });
+
+ let items = Array.from(frag.querySelectorAll("richlistitem"));
+
+ if (sortDirection === "descending") {
+ items.sort((a, b) => sortFunc(b, a));
+ } else {
+ items.sort(sortFunc);
+ }
+
+ // Re-append items in the correct order:
+ items.forEach(item => frag.appendChild(item));
+
+ let cols = list.previousElementSibling.querySelectorAll("treecol");
+ cols.forEach(c => {
+ c.removeAttribute("data-isCurrentSortCol");
+ c.removeAttribute("sortDirection");
+ });
+ column.setAttribute("data-isCurrentSortCol", "true");
+ column.setAttribute("sortDirection", sortDirection);
+ column.setAttribute("data-last-sortDirection", sortDirection);
+ },
+};
diff --git a/browser/components/preferences/dialogs/sitePermissions.xhtml b/browser/components/preferences/dialogs/sitePermissions.xhtml
new file mode 100644
index 0000000000..5cc307aced
--- /dev/null
+++ b/browser/components/preferences/dialogs/sitePermissions.xhtml
@@ -0,0 +1,115 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/preferences/dialogs/sitePermissions.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="SitePermissionsDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="permissions-window2"
+ data-l10n-attrs="title, style"
+ onload="gSitePermissionsManager.onLoad();"
+ onunload="gSitePermissionsManager.uninit();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept,cancel"
+ data-l10n-id="permission-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/preferences.ftl"
+ />
+ <html:link
+ rel="localization"
+ href="browser/preferences/permissions.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/sitePermissions.js" />
+ <script src="chrome://browser/content/preferences/extensionControlled.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="permissions-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <hbox align="center" id="setAutoplayPref" hidden="true">
+ <label data-l10n-id="permissions-autoplay-menu" />
+ </hbox>
+ <description id="permissionsText" control="url" />
+ <separator class="thin" />
+ <hbox align="start">
+ <search-textbox
+ id="searchBox"
+ flex="1"
+ data-l10n-id="permissions-searchbox"
+ data-l10n-attrs="placeholder"
+ oncommand="gSitePermissionsManager.buildPermissionsList();"
+ />
+ </hbox>
+ <separator class="thin" />
+ <listheader>
+ <treecol
+ id="siteCol"
+ data-l10n-id="permissions-site-name"
+ onclick="gSitePermissionsManager.buildPermissionsList(event.target)"
+ />
+ <treecol
+ id="statusCol"
+ data-l10n-id="permissions-status"
+ data-isCurrentSortCol="true"
+ onclick="gSitePermissionsManager.buildPermissionsList(event.target);"
+ />
+ </listheader>
+ <richlistbox
+ id="permissionsBox"
+ selected="false"
+ onkeypress="gSitePermissionsManager.onPermissionKeyPress(event);"
+ onselect="gSitePermissionsManager.onPermissionSelect();"
+ />
+ </vbox>
+
+ <hbox class="actionButtons">
+ <button
+ id="removePermission"
+ disabled="true"
+ data-l10n-id="permissions-remove"
+ oncommand="gSitePermissionsManager.onPermissionDelete();"
+ />
+ <button
+ id="removeAllPermissions"
+ data-l10n-id="permissions-remove-all"
+ oncommand="gSitePermissionsManager.onAllPermissionsDelete();"
+ />
+ </hbox>
+
+ <checkbox id="permissionsDisableCheckbox" />
+ <description id="permissionsDisableDescription" />
+ <hbox
+ id="browserNotificationsPermissionExtensionContent"
+ class="extension-controlled"
+ align="center"
+ hidden="true"
+ >
+ <description control="disableNotificationsPermissionExtension" flex="1" />
+ <button
+ id="disableNotificationsPermissionExtension"
+ class="extension-controlled-button accessory-button"
+ data-l10n-id="disable-extension"
+ />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/syncChooseWhatToSync.js b/browser/components/preferences/dialogs/syncChooseWhatToSync.js
new file mode 100644
index 0000000000..2cc965b4e1
--- /dev/null
+++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.js
@@ -0,0 +1,60 @@
+/* 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: "services.sync.engine.addons", type: "bool" },
+ { id: "services.sync.engine.bookmarks", type: "bool" },
+ { id: "services.sync.engine.history", type: "bool" },
+ { id: "services.sync.engine.tabs", type: "bool" },
+ { id: "services.sync.engine.prefs", type: "bool" },
+ { id: "services.sync.engine.passwords", type: "bool" },
+ { id: "services.sync.engine.addresses", type: "bool" },
+ { id: "services.sync.engine.creditcards", type: "bool" },
+]);
+
+let gSyncChooseWhatToSync = {
+ init() {
+ this._adjustForPrefs();
+ let options = window.arguments[0];
+ if (options.disconnectFun) {
+ // We offer 'disconnect'
+ document.addEventListener("dialogextra2", function () {
+ options.disconnectFun().then(disconnected => {
+ if (disconnected) {
+ window.close();
+ }
+ });
+ });
+ } else {
+ // no "disconnect" - hide the button.
+ document
+ .getElementById("syncChooseOptions")
+ .getButton("extra2").hidden = true;
+ }
+ },
+
+ // make whatever tweaks we need based on preferences.
+ _adjustForPrefs() {
+ // These 2 engines are unique in that there are prefs that make the
+ // entire engine unavailable (which is distinct from "disabled").
+ let enginePrefs = [
+ ["services.sync.engine.addresses", ".sync-engine-addresses"],
+ ["services.sync.engine.creditcards", ".sync-engine-creditcards"],
+ ];
+ for (let [enabledPref, className] of enginePrefs) {
+ let availablePref = enabledPref + ".available";
+ // If the engine is enabled we force it to be available, otherwise we see
+ // spooky things happen (like it magically re-appear later)
+ if (Services.prefs.getBoolPref(enabledPref, false)) {
+ Services.prefs.setBoolPref(availablePref, true);
+ }
+ if (!Services.prefs.getBoolPref(availablePref)) {
+ let elt = document.querySelector(className);
+ elt.hidden = true;
+ }
+ }
+ },
+};
diff --git a/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml
new file mode 100644
index 0000000000..0bed6913d4
--- /dev/null
+++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+
+<!-- -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 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/. -->
+
+<?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"
+ onload="gSyncChooseWhatToSync.init();"
+ data-l10n-id="sync-choose-what-to-sync-dialog3"
+ data-l10n-attrs="title, style"
+>
+ <dialog
+ id="syncChooseOptions"
+ buttons="accept,cancel,extra2"
+ data-l10n-id="sync-choose-what-to-sync-dialog3"
+ data-l10n-attrs="buttonlabelaccept, buttonlabelextra2"
+ >
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/preferences.ftl"
+ />
+ <html:link rel="localization" href="toolkit/branding/accounts.ftl" />
+ </linkset>
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.js" />
+ <description
+ class="sync-choose-dialog-description"
+ data-l10n-id="sync-choose-dialog-subtitle"
+ />
+ <html:div class="sync-engines-list">
+ <html:div class="sync-engine-bookmarks">
+ <checkbox
+ data-l10n-id="sync-engine-bookmarks"
+ preference="services.sync.engine.bookmarks"
+ />
+ </html:div>
+ <html:div class="sync-engine-history">
+ <checkbox
+ data-l10n-id="sync-engine-history"
+ preference="services.sync.engine.history"
+ />
+ </html:div>
+ <html:div class="sync-engine-tabs">
+ <checkbox
+ data-l10n-id="sync-engine-tabs"
+ preference="services.sync.engine.tabs"
+ />
+ </html:div>
+ <html:div class="sync-engine-passwords">
+ <checkbox
+ data-l10n-id="sync-engine-logins-passwords"
+ preference="services.sync.engine.passwords"
+ />
+ </html:div>
+ <html:div class="sync-engine-addresses">
+ <checkbox
+ data-l10n-id="sync-engine-addresses"
+ preference="services.sync.engine.addresses"
+ />
+ </html:div>
+ <html:div class="sync-engine-creditcards">
+ <checkbox
+ data-l10n-id="sync-engine-creditcards"
+ preference="services.sync.engine.creditcards"
+ />
+ </html:div>
+ <html:div class="sync-engine-addons">
+ <checkbox
+ data-l10n-id="sync-engine-addons"
+ preference="services.sync.engine.addons"
+ />
+ </html:div>
+ <html:div class="sync-engine-prefs">
+ <checkbox
+ data-l10n-id="sync-engine-settings"
+ preference="services.sync.engine.prefs"
+ />
+ </html:div>
+ </html:div>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/translationExceptions.js b/browser/components/preferences/dialogs/translationExceptions.js
new file mode 100644
index 0000000000..27579594c9
--- /dev/null
+++ b/browser/components/preferences/dialogs/translationExceptions.js
@@ -0,0 +1,256 @@
+/* -*- 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/. */
+
+// TODO (Bug 1817084) Remove this file when we disable the extension
+
+"use strict";
+
+const kPermissionType = "translate";
+const kLanguagesPref = "browser.translation.neverForLanguages";
+
+function Tree(aId, aData) {
+ this._data = aData;
+ this._tree = document.getElementById(aId);
+ this._tree.view = this;
+}
+
+Tree.prototype = {
+ get tree() {
+ return this._tree;
+ },
+ get isEmpty() {
+ return !this._data.length;
+ },
+ get hasSelection() {
+ return this.selection.count > 0;
+ },
+ getSelectedItems() {
+ let result = [];
+
+ let rc = this.selection.getRangeCount();
+ for (let i = 0; i < rc; ++i) {
+ let min = {},
+ max = {};
+ this.selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; ++j) {
+ result.push(this._data[j]);
+ }
+ }
+
+ return result;
+ },
+
+ // nsITreeView implementation
+ get rowCount() {
+ return this._data.length;
+ },
+ getCellText(aRow, aColumn) {
+ return this._data[aRow];
+ },
+ isSeparator(aIndex) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isContainer(aIndex) {
+ return false;
+ },
+ setTree(aTree) {},
+ getImageSrc(aRow, aColumn) {},
+ getCellValue(aRow, aColumn) {},
+ cycleHeader(column) {},
+ getRowProperties(row) {
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ getCellProperties(row, column) {
+ return "";
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
+};
+
+function Lang(aCode, label) {
+ this.langCode = aCode;
+ this._label = label;
+}
+
+Lang.prototype = {
+ toString() {
+ return this._label;
+ },
+};
+
+var gTranslationExceptions = {
+ onLoad() {
+ if (this._siteTree) {
+ // Re-using an open dialog, clear the old observers.
+ this.uninit();
+ }
+
+ // Load site permissions into an array.
+ this._sites = [];
+ for (let perm of Services.perms.all) {
+ if (
+ perm.type == kPermissionType &&
+ perm.capability == Services.perms.DENY_ACTION
+ ) {
+ this._sites.push(perm.principal.origin);
+ }
+ }
+ Services.obs.addObserver(this, "perm-changed");
+ this._sites.sort();
+
+ this._siteTree = new Tree("sitesTree", this._sites);
+ this.onSiteSelected();
+
+ this._langs = this.getLanguageExceptions();
+ Services.prefs.addObserver(kLanguagesPref, this);
+ this._langTree = new Tree("languagesTree", this._langs);
+ this.onLanguageSelected();
+ },
+
+ // Get the list of languages we don't translate as an array.
+ getLanguageExceptions() {
+ let langs = Services.prefs.getCharPref(kLanguagesPref);
+ if (!langs) {
+ return [];
+ }
+
+ let langArr = langs.split(",");
+ let displayNames = Services.intl.getLanguageDisplayNames(
+ undefined,
+ langArr
+ );
+ let result = langArr.map((lang, i) => new Lang(lang, displayNames[i]));
+ result.sort();
+
+ return result;
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "perm-changed") {
+ if (aData == "cleared") {
+ if (!this._sites.length) {
+ return;
+ }
+ let removed = this._sites.splice(0, this._sites.length);
+ this._siteTree.tree.rowCountChanged(0, -removed.length);
+ } else {
+ let perm = aSubject.QueryInterface(Ci.nsIPermission);
+ if (perm.type != kPermissionType) {
+ return;
+ }
+
+ if (aData == "added") {
+ if (perm.capability != Services.perms.DENY_ACTION) {
+ return;
+ }
+ this._sites.push(perm.principal.origin);
+ this._sites.sort();
+ let tree = this._siteTree.tree;
+ tree.rowCountChanged(0, 1);
+ tree.invalidate();
+ } else if (aData == "deleted") {
+ let index = this._sites.indexOf(perm.principal.origin);
+ if (index == -1) {
+ return;
+ }
+ this._sites.splice(index, 1);
+ this._siteTree.tree.rowCountChanged(index, -1);
+ this.onSiteSelected();
+ return;
+ }
+ }
+ this.onSiteSelected();
+ } else if (aTopic == "nsPref:changed") {
+ this._langs = this.getLanguageExceptions();
+ let change = this._langs.length - this._langTree.rowCount;
+ this._langTree._data = this._langs;
+ let tree = this._langTree.tree;
+ if (change) {
+ tree.rowCountChanged(0, change);
+ }
+ tree.invalidate();
+ this.onLanguageSelected();
+ }
+ },
+
+ _handleButtonDisabling(aTree, aIdPart) {
+ let empty = aTree.isEmpty;
+ document.getElementById("removeAll" + aIdPart + "s").disabled = empty;
+ document.getElementById("remove" + aIdPart).disabled =
+ empty || !aTree.hasSelection;
+ },
+
+ onLanguageSelected() {
+ this._handleButtonDisabling(this._langTree, "Language");
+ },
+
+ onSiteSelected() {
+ this._handleButtonDisabling(this._siteTree, "Site");
+ },
+
+ onLanguageDeleted() {
+ let langs = Services.prefs.getCharPref(kLanguagesPref);
+ if (!langs) {
+ return;
+ }
+
+ let removed = this._langTree.getSelectedItems().map(l => l.langCode);
+
+ langs = langs.split(",").filter(l => !removed.includes(l));
+ Services.prefs.setCharPref(kLanguagesPref, langs.join(","));
+ },
+
+ onAllLanguagesDeleted() {
+ Services.prefs.setCharPref(kLanguagesPref, "");
+ },
+
+ onSiteDeleted() {
+ let removedSites = this._siteTree.getSelectedItems();
+ for (let origin of removedSites) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ Services.perms.removeFromPrincipal(principal, kPermissionType);
+ }
+ },
+
+ onAllSitesDeleted() {
+ if (this._siteTree.isEmpty) {
+ return;
+ }
+
+ let removedSites = this._sites.splice(0, this._sites.length);
+ this._siteTree.tree.rowCountChanged(0, -removedSites.length);
+
+ for (let origin of removedSites) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ Services.perms.removeFromPrincipal(principal, kPermissionType);
+ }
+
+ this.onSiteSelected();
+ },
+
+ onSiteKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onSiteDeleted();
+ }
+ },
+
+ onLanguageKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onLanguageDeleted();
+ }
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "perm-changed");
+ Services.prefs.removeObserver(kLanguagesPref, this);
+ },
+};
diff --git a/browser/components/preferences/dialogs/translationExceptions.xhtml b/browser/components/preferences/dialogs/translationExceptions.xhtml
new file mode 100644
index 0000000000..b824ce86a5
--- /dev/null
+++ b/browser/components/preferences/dialogs/translationExceptions.xhtml
@@ -0,0 +1,127 @@
+<?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/. -->
+
+<!-- TODO (Bug 1817084) Remove this file when we disable the extension -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="TranslationDialog"
+ data-l10n-id="translation-window2"
+ data-l10n-attrs="title, style"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gTranslationExceptions.onLoad();"
+ onunload="gTranslationExceptions.uninit();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept"
+ data-l10n-id="translation-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link
+ rel="localization"
+ href="browser/preferences/translation.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/translationExceptions.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="translation-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <vbox flex="1">
+ <label
+ id="languagesLabel"
+ data-l10n-id="translation-languages-disabled-desc"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="languagesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationExceptions.onLanguageKeyPress(event)"
+ onselect="gTranslationExceptions.onLanguageSelected();"
+ >
+ <treecols>
+ <treecol
+ id="languageCol"
+ data-l10n-id="translation-languages-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ <hbox class="actionButtons" pack="end">
+ <button
+ id="removeLanguage"
+ disabled="true"
+ data-l10n-id="translation-languages-button-remove"
+ oncommand="gTranslationExceptions.onLanguageDeleted();"
+ />
+ <button
+ id="removeAllLanguages"
+ data-l10n-id="translation-languages-button-remove-all"
+ oncommand="gTranslationExceptions.onAllLanguagesDeleted();"
+ />
+ <spacer flex="1" />
+ </hbox>
+ <separator />
+ <vbox flex="1">
+ <label
+ id="languagesLabel"
+ data-l10n-id="translation-sites-disabled-desc"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="sitesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationExceptions.onSiteKeyPress(event)"
+ onselect="gTranslationExceptions.onSiteSelected();"
+ >
+ <treecols>
+ <treecol
+ id="siteCol"
+ data-l10n-id="translation-sites-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ </vbox>
+
+ <hbox class="actionButtons" pack="end">
+ <button
+ id="removeSite"
+ disabled="true"
+ data-l10n-id="translation-sites-button-remove"
+ oncommand="gTranslationExceptions.onSiteDeleted();"
+ />
+ <button
+ id="removeAllSites"
+ data-l10n-id="translation-sites-button-remove-all"
+ oncommand="gTranslationExceptions.onAllSitesDeleted();"
+ />
+ <spacer flex="1" />
+ </hbox>
+ </dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/translations.js b/browser/components/preferences/dialogs/translations.js
new file mode 100644
index 0000000000..30d2b22f17
--- /dev/null
+++ b/browser/components/preferences/dialogs/translations.js
@@ -0,0 +1,465 @@
+/* -*- 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/. */
+
+"use strict";
+
+/**
+ * The permission type to give to Services.perms for Translations.
+ */
+const TRANSLATIONS_PERMISSION = "translations";
+/**
+ * The list of BCP-47 language tags that will trigger auto-translate.
+ */
+const ALWAYS_TRANSLATE_LANGS_PREF =
+ "browser.translations.alwaysTranslateLanguages";
+/**
+ * The list of BCP-47 language tags that will prevent showing Translations UI.
+ */
+const NEVER_TRANSLATE_LANGS_PREF =
+ "browser.translations.neverTranslateLanguages";
+
+function Tree(aId, aData) {
+ this._data = aData;
+ this._tree = document.getElementById(aId);
+ this._tree.view = this;
+}
+
+Tree.prototype = {
+ get tree() {
+ return this._tree;
+ },
+ get isEmpty() {
+ return !this._data.length;
+ },
+ get hasSelection() {
+ return this.selection.count > 0;
+ },
+ getSelectedItems() {
+ let result = [];
+
+ let rc = this.selection.getRangeCount();
+ for (let i = 0; i < rc; ++i) {
+ let min = {},
+ max = {};
+ this.selection.getRangeAt(i, min, max);
+ for (let j = min.value; j <= max.value; ++j) {
+ result.push(this._data[j]);
+ }
+ }
+
+ return result;
+ },
+
+ // nsITreeView implementation
+ get rowCount() {
+ return this._data.length;
+ },
+ getCellText(aRow, aColumn) {
+ return this._data[aRow];
+ },
+ isSeparator(aIndex) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isContainer(aIndex) {
+ return false;
+ },
+ setTree(aTree) {},
+ getImageSrc(aRow, aColumn) {},
+ getCellValue(aRow, aColumn) {},
+ cycleHeader(column) {},
+ getRowProperties(row) {
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ getCellProperties(row, column) {
+ return "";
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
+};
+
+function Lang(aCode, label) {
+ this.langCode = aCode;
+ this._label = label;
+}
+
+Lang.prototype = {
+ toString() {
+ return this._label;
+ },
+};
+
+var gTranslationsSettings = {
+ onLoad() {
+ if (this._neverTranslateSiteTree) {
+ // Re-using an open dialog, clear the old observers.
+ this.removeObservers();
+ }
+
+ // Load site permissions into an array.
+ this._neverTranslateSites = [];
+ for (const perm of Services.perms.getAllByTypes([
+ TRANSLATIONS_PERMISSION,
+ ])) {
+ if (perm.capability === Services.perms.DENY_ACTION) {
+ this._neverTranslateSites.push(perm.principal.origin);
+ }
+ }
+ let stripProtocol = s => s?.replace(/^\w+:/, "") || "";
+ this._neverTranslateSites.sort((a, b) => {
+ return stripProtocol(a).localeCompare(stripProtocol(b));
+ });
+
+ // Load language tags into arrays.
+ this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages();
+ this._neverTranslateLangs = this.getNeverTranslateLanguages();
+
+ // Add observers for relevant prefs and permissions.
+ Services.obs.addObserver(this, "perm-changed");
+ Services.prefs.addObserver(ALWAYS_TRANSLATE_LANGS_PREF, this);
+ Services.prefs.addObserver(NEVER_TRANSLATE_LANGS_PREF, this);
+
+ // Build trees from the arrays.
+ this._alwaysTranslateLangsTree = new Tree(
+ "alwaysTranslateLanguagesTree",
+ this._alwaysTranslateLangs
+ );
+ this._neverTranslateLangsTree = new Tree(
+ "neverTranslateLanguagesTree",
+ this._neverTranslateLangs
+ );
+ this._neverTranslateSiteTree = new Tree(
+ "neverTranslateSitesTree",
+ this._neverTranslateSites
+ );
+
+ // Ensure the UI for each group is in the correct state.
+ this.onSelectAlwaysTranslateLanguage();
+ this.onSelectNeverTranslateLanguage();
+ this.onSelectNeverTranslateSite();
+ },
+
+ /**
+ * Retrieves the value of a char-pref splits its value into an
+ * array delimited by commas.
+ *
+ * This is used for the translations preferences which are comma-
+ * separated lists of BCP-47 language tags.
+ *
+ * @param {string} pref
+ * @returns {Array<string>}
+ */
+ getLangsFromPref(pref) {
+ let rawLangs = Services.prefs.getCharPref(pref);
+ if (!rawLangs) {
+ return [];
+ }
+
+ let langArr = rawLangs.split(",");
+ let displayNames = Services.intl.getLanguageDisplayNames(
+ undefined,
+ langArr
+ );
+ let langs = langArr.map((lang, i) => new Lang(lang, displayNames[i]));
+ langs.sort();
+
+ return langs;
+ },
+
+ /**
+ * Retrieves the always-translate language tags as an array.
+ * @returns {Array<string>}
+ */
+ getAlwaysTranslateLanguages() {
+ return this.getLangsFromPref(ALWAYS_TRANSLATE_LANGS_PREF);
+ },
+
+ /**
+ * Retrieves the never-translate language tags as an array.
+ * @returns {Array<string>}
+ */
+ getNeverTranslateLanguages() {
+ return this.getLangsFromPref(NEVER_TRANSLATE_LANGS_PREF);
+ },
+
+ /**
+ * Handles updating the UI components on pref or permission changes.
+ */
+ observe(aSubject, aTopic, aData) {
+ if (aTopic === "perm-changed") {
+ if (aData === "cleared") {
+ // Permissions have been cleared
+ if (!this._neverTranslateSites.length) {
+ // There were no sites with permissions set, nothing to do.
+ return;
+ }
+ // Update the tree based on the amount of permissions removed.
+ let removed = this._neverTranslateSites.splice(
+ 0,
+ this._neverTranslateSites.length
+ );
+ this._neverTranslateSiteTree.tree.rowCountChanged(0, -removed.length);
+ } else {
+ let perm = aSubject.QueryInterface(Ci.nsIPermission);
+ if (perm.type != TRANSLATIONS_PERMISSION) {
+ // The updated permission was not for Translations, nothing to do.
+ return;
+ }
+ if (aData === "added") {
+ if (perm.capability != Services.perms.DENY_ACTION) {
+ // We are only showing data for sites we should never translate.
+ // If the permission is not DENY_ACTION, we don't care about it here.
+ return;
+ }
+ this._neverTranslateSites.push(perm.principal.origin);
+ this._neverTranslateSites.sort();
+ let tree = this._neverTranslateSiteTree.tree;
+ tree.rowCountChanged(0, 1);
+ tree.invalidate();
+ } else if (aData == "deleted") {
+ let index = this._neverTranslateSites.indexOf(perm.principal.origin);
+ if (index == -1) {
+ // The deleted permission was not in the tree, nothing to do.
+ return;
+ }
+ this._neverTranslateSites.splice(index, 1);
+ this._neverTranslateSiteTree.tree.rowCountChanged(index, -1);
+ }
+ }
+ // Ensure the UI updates to the changes.
+ this.onSelectNeverTranslateSite();
+ } else if (aTopic === "nsPref:changed") {
+ switch (aData) {
+ case ALWAYS_TRANSLATE_LANGS_PREF: {
+ this._alwaysTranslateLangs = this.getAlwaysTranslateLanguages();
+
+ let alwaysTranslateLangsChange =
+ this._alwaysTranslateLangs.length -
+ this._alwaysTranslateLangsTree.rowCount;
+
+ this._alwaysTranslateLangsTree._data = this._alwaysTranslateLangs;
+ let alwaysTranslateLangsTree = this._alwaysTranslateLangsTree.tree;
+
+ if (alwaysTranslateLangsChange) {
+ alwaysTranslateLangsTree.rowCountChanged(
+ 0,
+ alwaysTranslateLangsChange
+ );
+ }
+
+ alwaysTranslateLangsTree.invalidate();
+
+ // Ensure the UI updates to the changes.
+ this.onSelectAlwaysTranslateLanguage();
+ break;
+ }
+ case NEVER_TRANSLATE_LANGS_PREF: {
+ this._neverTranslateLangs = this.getNeverTranslateLanguages();
+
+ let neverTranslateLangsChange =
+ this._neverTranslateLangs.length -
+ this._neverTranslateLangsTree.rowCount;
+
+ this._neverTranslateLangsTree._data = this._neverTranslateLangs;
+ let neverTranslateLangsTree = this._neverTranslateLangsTree.tree;
+
+ if (neverTranslateLangsChange) {
+ neverTranslateLangsTree.rowCountChanged(
+ 0,
+ neverTranslateLangsChange
+ );
+ }
+
+ neverTranslateLangsTree.invalidate();
+
+ // Ensure the UI updates to the changes.
+ this.onSelectNeverTranslateLanguage();
+ break;
+ }
+ }
+ }
+ },
+
+ /**
+ * Ensures that buttons states are enabled/disabled accordingly based on the
+ * content of the trees.
+ *
+ * The remove button should be enabled only if an item is selected.
+ * The removeAll button should be enabled any time the tree has content.
+ *
+ * @param {Tree} aTree
+ * @param {string} aIdPart
+ */
+ _handleButtonDisabling(aTree, aIdPart) {
+ let empty = aTree.isEmpty;
+ document.getElementById("removeAll" + aIdPart + "s").disabled = empty;
+ document.getElementById("remove" + aIdPart).disabled =
+ empty || !aTree.hasSelection;
+ },
+
+ /**
+ * Updates the UI state for the always-translate languages section.
+ */
+ onSelectAlwaysTranslateLanguage() {
+ this._handleButtonDisabling(
+ this._alwaysTranslateLangsTree,
+ "AlwaysTranslateLanguage"
+ );
+ },
+
+ /**
+ * Updates the UI state for the never-translate languages section.
+ */
+ onSelectNeverTranslateLanguage() {
+ this._handleButtonDisabling(
+ this._neverTranslateLangsTree,
+ "NeverTranslateLanguage"
+ );
+ },
+
+ /**
+ * Updates the UI state for the never-translate sites section.
+ */
+ onSelectNeverTranslateSite() {
+ this._handleButtonDisabling(
+ this._neverTranslateSiteTree,
+ "NeverTranslateSite"
+ );
+ },
+
+ /**
+ * Updates the value of a language pref to match when a language is removed
+ * through the UI.
+ *
+ * @param {string} pref
+ * @param {Tree} tree
+ */
+ _onRemoveLanguage(pref, tree) {
+ let langs = Services.prefs.getCharPref(pref);
+ if (!langs) {
+ return;
+ }
+
+ let removed = tree.getSelectedItems().map(l => l.langCode);
+
+ langs = langs.split(",").filter(l => !removed.includes(l));
+ Services.prefs.setCharPref(pref, langs.join(","));
+ },
+
+ /**
+ * Updates the never-translate language pref when a never-translate language
+ * is removed via the UI.
+ */
+ onRemoveAlwaysTranslateLanguage() {
+ this._onRemoveLanguage(
+ ALWAYS_TRANSLATE_LANGS_PREF,
+ this._alwaysTranslateLangsTree
+ );
+ },
+
+ /**
+ * Updates the always-translate language pref when a always-translate language
+ * is removed via the UI.
+ */
+ onRemoveNeverTranslateLanguage() {
+ this._onRemoveLanguage(
+ NEVER_TRANSLATE_LANGS_PREF,
+ this._neverTranslateLangsTree
+ );
+ },
+
+ /**
+ * Updates the permissions for a never-translate site when it is removed via the UI.
+ */
+ onRemoveNeverTranslateSite() {
+ let removedNeverTranslateSites =
+ this._neverTranslateSiteTree.getSelectedItems();
+ for (let origin of removedNeverTranslateSites) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION);
+ }
+ },
+
+ /**
+ * Clears the always-translate languages pref when the list is cleared in the UI.
+ */
+ onRemoveAllAlwaysTranslateLanguages() {
+ Services.prefs.setCharPref(ALWAYS_TRANSLATE_LANGS_PREF, "");
+ },
+
+ /**
+ * Clears the never-translate languages pref when the list is cleared in the UI.
+ */
+ onRemoveAllNeverTranslateLanguages() {
+ Services.prefs.setCharPref(NEVER_TRANSLATE_LANGS_PREF, "");
+ },
+
+ /**
+ * Clears the never-translate sites pref when the list is cleared in the UI.
+ */
+ onRemoveAllNeverTranslateSites() {
+ if (this._neverTranslateSiteTree.isEmpty) {
+ return;
+ }
+
+ let removedNeverTranslateSites = this._neverTranslateSites.splice(
+ 0,
+ this._neverTranslateSites.length
+ );
+ this._neverTranslateSiteTree.tree.rowCountChanged(
+ 0,
+ -removedNeverTranslateSites.length
+ );
+
+ for (let origin of removedNeverTranslateSites) {
+ let principal =
+ Services.scriptSecurityManager.createContentPrincipalFromOrigin(origin);
+ Services.perms.removeFromPrincipal(principal, TRANSLATIONS_PERMISSION);
+ }
+
+ this.onSelectNeverTranslateSite();
+ },
+
+ /**
+ * Handles removing a selected always-translate language via the keyboard.
+ */
+ onAlwaysTranslateLanguageKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onRemoveAlwaysTranslateLanguage();
+ }
+ },
+
+ /**
+ * Handles removing a selected never-translate language via the keyboard.
+ */
+ onNeverTranslateLanguageKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onRemoveNeverTranslateLanguage();
+ }
+ },
+
+ /**
+ * Handles removing a selected never-translate site via the keyboard.
+ */
+ onNeverTranslateSiteKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE) {
+ this.onRemoveNeverTranslateSite();
+ }
+ },
+
+ /**
+ * Removes any active preference and permissions observers.
+ */
+ removeObservers() {
+ Services.obs.removeObserver(this, "perm-changed");
+ Services.prefs.removeObserver(ALWAYS_TRANSLATE_LANGS_PREF, this);
+ Services.prefs.removeObserver(NEVER_TRANSLATE_LANGS_PREF, this);
+ },
+};
diff --git a/browser/components/preferences/dialogs/translations.xhtml b/browser/components/preferences/dialogs/translations.xhtml
new file mode 100644
index 0000000000..205bd54ac1
--- /dev/null
+++ b/browser/components/preferences/dialogs/translations.xhtml
@@ -0,0 +1,158 @@
+<?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" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css" type="text/css"?>
+
+<window
+ id="TranslationsDialog"
+ data-l10n-id="translations-settings-title"
+ data-l10n-attrs="title, style"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gTranslationsSettings.onLoad();"
+ onunload="gTranslationsSettings.removeObservers();"
+ persist="width height"
+>
+ <dialog
+ buttons="accept"
+ data-l10n-id="translations-settings-close-dialog"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept"
+ >
+ <linkset>
+ <html:link rel="localization" href="browser/translations.ftl" />
+ </linkset>
+
+ <script src="chrome://browser/content/preferences/dialogs/translations.js" />
+
+ <keyset>
+ <key
+ data-l10n-id="translations-settings-close-key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane">
+ <vbox flex="1">
+ <label
+ id="alwaysTranslateLanguagesLabel"
+ data-l10n-id="translations-settings-always-translate-langs-description"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="alwaysTranslateLanguagesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationsSettings.onAlwaysTranslateLanguageKeyPress(event)"
+ onselect="gTranslationsSettings.onSelectAlwaysTranslateLanguage();"
+ >
+ <treecols>
+ <treecol
+ id="languageCol"
+ data-l10n-id="translations-settings-languages-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ <hbox class="actionButtons" pack="start">
+ <button
+ id="removeAlwaysTranslateLanguage"
+ disabled="true"
+ data-l10n-id="translations-settings-remove-language-button"
+ oncommand="gTranslationsSettings.onRemoveAlwaysTranslateLanguage();"
+ />
+ <button
+ id="removeAllAlwaysTranslateLanguages"
+ data-l10n-id="translations-settings-remove-all-languages-button"
+ oncommand="gTranslationsSettings.onRemoveAllAlwaysTranslateLanguages();"
+ />
+ </hbox>
+ <separator />
+ <vbox flex="1">
+ <label
+ id="neverTranslateLanguagesLabel"
+ data-l10n-id="translations-settings-never-translate-langs-description"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="neverTranslateLanguagesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationsSettings.onNeverTranslateLanguageKeyPress(event)"
+ onselect="gTranslationsSettings.onSelectNeverTranslateLanguage();"
+ >
+ <treecols>
+ <treecol
+ id="languageCol"
+ data-l10n-id="translations-settings-languages-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ <hbox class="actionButtons" pack="start">
+ <button
+ id="removeNeverTranslateLanguage"
+ disabled="true"
+ data-l10n-id="translations-settings-remove-language-button"
+ oncommand="gTranslationsSettings.onRemoveNeverTranslateLanguage();"
+ />
+ <button
+ id="removeAllNeverTranslateLanguages"
+ data-l10n-id="translations-settings-remove-all-languages-button"
+ oncommand="gTranslationsSettings.onRemoveAllNeverTranslateLanguages();"
+ />
+ </hbox>
+ <separator />
+ <vbox flex="1">
+ <label
+ id="neverTranslateSitesLabel"
+ data-l10n-id="translations-settings-never-translate-sites-description"
+ control="permissionsTree"
+ />
+ <separator class="thin" />
+ <tree
+ id="neverTranslateSitesTree"
+ flex="1"
+ style="height: 12em"
+ hidecolumnpicker="true"
+ onkeypress="gTranslationsSettings.onNeverTranslateSiteKeyPress(event)"
+ onselect="gTranslationsSettings.onSelectNeverTranslateSite();"
+ >
+ <treecols>
+ <treecol
+ id="siteCol"
+ data-l10n-id="translations-settings-sites-column"
+ flex="1"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ <hbox class="actionButtons" pack="start">
+ <button
+ id="removeNeverTranslateSite"
+ disabled="true"
+ data-l10n-id="translations-settings-remove-site-button"
+ oncommand="gTranslationsSettings.onRemoveNeverTranslateSite();"
+ />
+ <button
+ id="removeAllNeverTranslateSites"
+ data-l10n-id="translations-settings-remove-all-sites-button"
+ oncommand="gTranslationsSettings.onRemoveAllNeverTranslateSites();"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+</window>