summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/dialogs
diff options
context:
space:
mode:
Diffstat (limited to '')
-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.xhtml51
-rw-r--r--browser/components/preferences/dialogs/applicationManager.js129
-rw-r--r--browser/components/preferences/dialogs/applicationManager.xhtml52
-rw-r--r--browser/components/preferences/dialogs/blocklists.js175
-rw-r--r--browser/components/preferences/dialogs/blocklists.xhtml53
-rw-r--r--browser/components/preferences/dialogs/browserLanguages.js744
-rw-r--r--browser/components/preferences/dialogs/browserLanguages.xhtml55
-rw-r--r--browser/components/preferences/dialogs/clearSiteData.css20
-rw-r--r--browser/components/preferences/dialogs/clearSiteData.js98
-rw-r--r--browser/components/preferences/dialogs/clearSiteData.xhtml49
-rw-r--r--browser/components/preferences/dialogs/colors.js19
-rw-r--r--browser/components/preferences/dialogs/colors.xhtml93
-rw-r--r--browser/components/preferences/dialogs/connection.js653
-rw-r--r--browser/components/preferences/dialogs/connection.xhtml159
-rw-r--r--browser/components/preferences/dialogs/containers.js167
-rw-r--r--browser/components/preferences/dialogs/containers.xhtml53
-rw-r--r--browser/components/preferences/dialogs/fonts.js173
-rw-r--r--browser/components/preferences/dialogs/fonts.xhtml222
-rw-r--r--browser/components/preferences/dialogs/handlers.css21
-rw-r--r--browser/components/preferences/dialogs/jar.mn45
-rw-r--r--browser/components/preferences/dialogs/languages.js382
-rw-r--r--browser/components/preferences/dialogs/languages.xhtml67
-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.xhtml83
-rw-r--r--browser/components/preferences/dialogs/sanitize.js38
-rw-r--r--browser/components/preferences/dialogs/sanitize.xhtml73
-rw-r--r--browser/components/preferences/dialogs/selectBookmark.js119
-rw-r--r--browser/components/preferences/dialogs/selectBookmark.xhtml49
-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.js337
-rw-r--r--browser/components/preferences/dialogs/siteDataSettings.xhtml59
-rw-r--r--browser/components/preferences/dialogs/sitePermissions.css40
-rw-r--r--browser/components/preferences/dialogs/sitePermissions.js604
-rw-r--r--browser/components/preferences/dialogs/sitePermissions.xhtml82
-rw-r--r--browser/components/preferences/dialogs/syncChooseWhatToSync.js60
-rw-r--r--browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml63
-rw-r--r--browser/components/preferences/dialogs/translation.js256
-rw-r--r--browser/components/preferences/dialogs/translation.xhtml87
42 files changed, 6285 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..8d5f5bfe3e
--- /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 {
+ -moz-box-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..1b3b1be591
--- /dev/null
+++ b/browser/components/preferences/dialogs/addEngine.xhtml
@@ -0,0 +1,51 @@
+<?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..5828bd6258
--- /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..bd2f6f590e
--- /dev/null
+++ b/browser/components/preferences/dialogs/applicationManager.xhtml
@@ -0,0 +1,52 @@
+<?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..1612f8f1a6
--- /dev/null
+++ b/browser/components/preferences/dialogs/blocklists.xhtml
@@ -0,0 +1,53 @@
+<?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="-moz-box-flex: 1" sortable="false"
+ type="checkbox"/>
+ <treecol id="listCol" data-l10n-id="blocklist-treehead-list" style="-moz-box-flex: 80"
+ 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..3aa4f9af9c
--- /dev/null
+++ b/browser/components/preferences/dialogs/browserLanguages.js
@@ -0,0 +1,744 @@
+/* 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.defineModuleGetter(
+ this,
+ "AddonManager",
+ "resource://gre/modules/AddonManager.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "RemoteSettings",
+ "resource://services-settings/remote-settings.js"
+);
+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..2f5afc6fca
--- /dev/null
+++ b/browser/components/preferences/dialogs/browserLanguages.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://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 flex="1" style="display: grid; grid-template-rows: 1fr auto; grid-template-columns: 1fr auto;">
+ <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..144408ca0b
--- /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(--in-content-deemphasized-text);
+ 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..ca0d65d86d
--- /dev/null
+++ b/browser/components/preferences/dialogs/clearSiteData.js
@@ -0,0 +1,98 @@
+/* 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.defineModuleGetter(
+ this,
+ "DownloadUtils",
+ "resource://gre/modules/DownloadUtils.jsm"
+);
+
+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..a77176acf8
--- /dev/null
+++ b/browser/components/preferences/dialogs/clearSiteData.xhtml
@@ -0,0 +1,49 @@
+<?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..0c2e3ae1ba
--- /dev/null
+++ b/browser/components/preferences/dialogs/colors.xhtml
@@ -0,0 +1,93 @@
+<?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..06c070dc54
--- /dev/null
+++ b/browser/components/preferences/dialogs/connection.js
@@ -0,0 +1,653 @@
+/* -*- 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 */
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "DoHConfigController",
+ "resource:///modules/DoHConfig.jsm"
+);
+
+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" },
+ { id: "network.trr.mode", type: "int" },
+ { id: "network.trr.uri", type: "string" },
+ { id: "network.trr.custom_uri", type: "string" },
+ { id: "doh-rollout.disable-heuristics", type: "bool" },
+ { id: "doh-rollout.skipHeuristicsCheck", type: "bool" },
+]);
+
+const DoHConfigObserver = () => {
+ gConnectionsDialog.initDnsOverHttpsUI();
+};
+
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ Preferences.get("network.proxy.type").on(
+ "change",
+ gConnectionsDialog.proxyTypeChanged.bind(gConnectionsDialog)
+ );
+ Preferences.get("network.proxy.socks_version").on(
+ "change",
+ gConnectionsDialog.updateDNSPref.bind(gConnectionsDialog)
+ );
+
+ Preferences.get("network.trr.uri").on("change", () => {
+ gConnectionsDialog.updateDnsOverHttpsUI();
+ });
+
+ Services.obs.addObserver(
+ DoHConfigObserver,
+ DoHConfigController.kConfigUpdateTopic
+ );
+ window.addEventListener(
+ "unload",
+ e => {
+ Services.obs.removeObserver(
+ DoHConfigObserver,
+ DoHConfigController.kConfigUpdateTopic
+ );
+ },
+ { once: true }
+ );
+
+ // XXX: We can't init the DNS-over-HTTPs UI until the onsyncfrompreference for network.trr.mode
+ // has been called. The uiReady promise will be resolved after the first call to
+ // readDnsOverHttpsMode and the subsequent call to initDnsOverHttpsUI has happened.
+ gConnectionsDialog.uiReady = new Promise(resolve => {
+ gConnectionsDialog._areTrrPrefsReady = false;
+ gConnectionsDialog._handleTrrPrefsReady = resolve;
+ }).then(async () => {
+ // awaiting this ensures that initDnsOverHttpsUI() is called after
+ // execution has been returned to the caller of _handleTrrPrefsReady,
+ // which is the checkbox value reading path. This ensures the checkbox
+ // gets checked, then initDnsOverHttpsUI() is called, then the uiReady
+ // promise resolves, preventing intermittent failures in tests.
+ await gConnectionsDialog.initDnsOverHttpsUI();
+ });
+
+ 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) {
+ let dnsOverHttpsResolverChoice = document.getElementById(
+ "networkDnsOverHttpsResolverChoices"
+ ).value;
+ let writeURIandMode = uri => {
+ Services.prefs.setStringPref("network.trr.uri", uri);
+ // When writing the URI, also write the mode. This is needed in addition
+ // to the mode reacting in realtime to the checkbox state because of the
+ // case when the checkbox was ticked due to the rollout being enabled at
+ // the time of clicking "Accept".
+ Services.prefs.setIntPref(
+ "network.trr.mode",
+ this.writeDnsOverHttpsMode()
+ );
+ };
+ // We treat clicking "Accept" as a user choice, and set both the TRR
+ // URI and mode here. This will cause DoHController to permanently
+ // disable heuristics and the values at the time of accept will persist.
+ // This includes the case when no changes were made.
+ if (dnsOverHttpsResolverChoice == "custom") {
+ let customValue = document
+ .getElementById("networkCustomDnsOverHttpsInput")
+ .value.trim();
+ if (customValue) {
+ writeURIandMode(customValue);
+ } else {
+ writeURIandMode(DoHConfigController.currentConfig.fallbackProviderURI);
+ }
+ } else {
+ writeURIandMode(dnsOverHttpsResolverChoice);
+ }
+
+ var proxyTypePref = Preferences.get("network.proxy.type");
+ if (proxyTypePref.value == 2) {
+ this.doAutoconfigURLFixup();
+ return;
+ }
+
+ if (proxyTypePref.value != 1) {
+ return;
+ }
+
+ var httpProxyURLPref = Preferences.get("network.proxy.http");
+ var httpProxyPortPref = Preferences.get("network.proxy.http_port");
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+
+ // If the port is 0 and the proxy server is specified, focus on the port and cancel submission.
+ for (let prefName of ["http", "ssl", "socks"]) {
+ let proxyPortPref = Preferences.get(
+ "network.proxy." + prefName + "_port"
+ );
+ let proxyPref = Preferences.get("network.proxy." + prefName);
+ // Only worry about ports which are currently active. If the share option is on, then ignore
+ // all ports except the HTTP and SOCKS port
+ if (
+ proxyPref.value != "" &&
+ proxyPortPref.value == 0 &&
+ (prefName == "http" || prefName == "socks" || !shareProxiesPref.value)
+ ) {
+ document
+ .getElementById("networkProxy" + prefName.toUpperCase() + "_Port")
+ .focus();
+ event.preventDefault();
+ return;
+ }
+ }
+
+ // In the case of a shared proxy preference, backup the current values and update with the HTTP value
+ if (shareProxiesPref.value) {
+ var proxyServerURLPref = Preferences.get("network.proxy.ssl");
+ var proxyPortPref = Preferences.get("network.proxy.ssl_port");
+ var backupServerURLPref = Preferences.get("network.proxy.backup.ssl");
+ var backupPortPref = Preferences.get("network.proxy.backup.ssl_port");
+ backupServerURLPref.value =
+ backupServerURLPref.value || proxyServerURLPref.value;
+ backupPortPref.value = backupPortPref.value || proxyPortPref.value;
+ proxyServerURLPref.value = httpProxyURLPref.value;
+ proxyPortPref.value = httpProxyPortPref.value;
+ }
+
+ this.sanitizeNoProxiesPref();
+ },
+
+ checkForSystemProxy() {
+ if ("@mozilla.org/system-proxy-settings;1" in Cc) {
+ document.getElementById("systemPref").removeAttribute("hidden");
+ }
+ },
+
+ proxyTypeChanged() {
+ var proxyTypePref = Preferences.get("network.proxy.type");
+
+ // Update http
+ var httpProxyURLPref = Preferences.get("network.proxy.http");
+ httpProxyURLPref.updateControlDisabledState(proxyTypePref.value != 1);
+ var httpProxyPortPref = Preferences.get("network.proxy.http_port");
+ httpProxyPortPref.updateControlDisabledState(proxyTypePref.value != 1);
+
+ // Now update the other protocols
+ this.updateProtocolPrefs();
+
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ shareProxiesPref.updateControlDisabledState(proxyTypePref.value != 1);
+ var autologinProxyPref = Preferences.get("signon.autologin.proxy");
+ autologinProxyPref.updateControlDisabledState(proxyTypePref.value == 0);
+ var noProxiesPref = Preferences.get("network.proxy.no_proxies_on");
+ noProxiesPref.updateControlDisabledState(proxyTypePref.value == 0);
+
+ var autoconfigURLPref = Preferences.get("network.proxy.autoconfig_url");
+ autoconfigURLPref.updateControlDisabledState(proxyTypePref.value != 2);
+
+ this.updateReloadButton();
+
+ document.getElementById(
+ "networkProxyNoneLocalhost"
+ ).hidden = Services.prefs.getBoolPref(
+ "network.proxy.allow_hijacking_localhost",
+ false
+ );
+ },
+
+ updateDNSPref() {
+ var socksVersionPref = Preferences.get("network.proxy.socks_version");
+ var socksDNSPref = Preferences.get("network.proxy.socks_remote_dns");
+ var proxyTypePref = Preferences.get("network.proxy.type");
+ var isDefinitelySocks4 =
+ proxyTypePref.value == 1 && socksVersionPref.value == 4;
+ socksDNSPref.updateControlDisabledState(
+ isDefinitelySocks4 || proxyTypePref.value == 0
+ );
+ return undefined;
+ },
+
+ updateReloadButton() {
+ // Disable the "Reload PAC" button if the selected proxy type is not PAC or
+ // if the current value of the PAC 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
+ );
+ }
+ },
+
+ get dnsOverHttpsResolvers() {
+ let providers = DoHConfigController.currentConfig.providerList;
+ // if there's no default, we'll hold its position with an empty string
+ let defaultURI = DoHConfigController.currentConfig.fallbackProviderURI;
+ let defaultIndex = providers.findIndex(p => p.uri == defaultURI);
+ if (defaultIndex == -1 && defaultURI) {
+ // the default value for the pref isn't included in the resolvers list
+ // so we'll make a stub for it. Without an id, we'll have to use the url as the label
+ providers.unshift({ uri: defaultURI });
+ }
+ return providers;
+ },
+
+ isDnsOverHttpsLocked() {
+ return Services.prefs.prefIsLocked("network.trr.mode");
+ },
+
+ isDnsOverHttpsEnabled() {
+ // We consider DoH enabled if:
+ // 1. network.trr.mode has a user-set value equal to 2 or 3.
+ // 2. network.trr.mode is 0, and DoH heuristics are enabled
+ let trrPref = Preferences.get("network.trr.mode");
+ if (trrPref.value > 0) {
+ return trrPref.value == 2 || trrPref.value == 3;
+ }
+
+ let rolloutEnabled = DoHConfigController.currentConfig.enabled;
+ let heuristicsDisabled =
+ Preferences.get("doh-rollout.disable-heuristics").value ||
+ Preferences.get("doh-rollout.skipHeuristicsCheck").value;
+ return rolloutEnabled && !heuristicsDisabled;
+ },
+
+ readDnsOverHttpsMode() {
+ // called to update checked element property to reflect current pref value
+ let enabled = this.isDnsOverHttpsEnabled();
+ let uriPref = Preferences.get("network.trr.uri");
+ uriPref.updateControlDisabledState(!enabled || this.isDnsOverHttpsLocked());
+ // this is the first signal we get when the prefs are available, so
+ // lazy-init if appropriate
+ if (!this._areTrrPrefsReady) {
+ this._areTrrPrefsReady = true;
+ this._handleTrrPrefsReady();
+ } else {
+ this.updateDnsOverHttpsUI();
+ }
+ return enabled;
+ },
+
+ writeDnsOverHttpsMode() {
+ // called to update pref with user change
+ let trrModeCheckbox = document.getElementById("networkDnsOverHttps");
+
+ let trrModeCurrent = Preferences.get("network.trr.mode").value;
+ if (trrModeCheckbox.checked) {
+ //Check if the user has set the value themself through about:config.
+ if (trrModeCurrent == Ci.nsIDNSService.MODE_TRRONLY) {
+ return Ci.nsIDNSService.MODE_TRRONLY;
+ }
+ // we treat checked/enabled as mode 2
+ return Ci.nsIDNSService.MODE_TRRFIRST;
+ }
+
+ return Ci.nsIDNSService.MODE_TRROFF;
+ },
+
+ updateDnsOverHttpsUI() {
+ // init and update of the UI must wait until the pref values are ready
+ if (!this._areTrrPrefsReady) {
+ return;
+ }
+ let [menu, customInput] = this.getDnsOverHttpsControls();
+ let customDohContainer = document.getElementById(
+ "customDnsOverHttpsContainer"
+ );
+ let customURI = Preferences.get("network.trr.custom_uri").value;
+ let currentURI = Preferences.get("network.trr.uri").value;
+ let resolvers = this.dnsOverHttpsResolvers;
+ let isCustom = menu.value == "custom";
+
+ if (this.isDnsOverHttpsEnabled()) {
+ this.toggleDnsOverHttpsUI(false);
+ if (isCustom) {
+ // if the current and custom_uri values mismatch, update the uri pref
+ if (
+ currentURI &&
+ !customURI &&
+ !resolvers.find(r => r.uri == currentURI)
+ ) {
+ Services.prefs.setStringPref("network.trr.custom_uri", currentURI);
+ }
+ }
+ } else {
+ this.toggleDnsOverHttpsUI(true);
+ }
+
+ if (!menu.disabled && isCustom) {
+ customDohContainer.hidden = false;
+ customInput.disabled = false;
+ customInput.scrollIntoView();
+ } else {
+ customDohContainer.hidden = true;
+ customInput.disabled = true;
+ }
+
+ // The height has likely changed, find our SubDialog and tell it to resize.
+ requestAnimationFrame(() => {
+ let dialogs = window.opener.gSubDialog._dialogs;
+ let dialog = dialogs.find(d => d._frame.contentDocument == document);
+ if (dialog) {
+ dialog.resizeVertically();
+ }
+ });
+ },
+
+ getDnsOverHttpsControls() {
+ return [
+ document.getElementById("networkDnsOverHttpsResolverChoices"),
+ document.getElementById("networkCustomDnsOverHttpsInput"),
+ document.getElementById("networkDnsOverHttpsResolverChoicesLabel"),
+ document.getElementById("networkCustomDnsOverHttpsInputLabel"),
+ ];
+ },
+
+ toggleDnsOverHttpsUI(disabled) {
+ for (let element of this.getDnsOverHttpsControls()) {
+ element.disabled = disabled;
+ }
+ },
+
+ initDnsOverHttpsUI() {
+ let resolvers = this.dnsOverHttpsResolvers;
+ let defaultURI = DoHConfigController.currentConfig.fallbackProviderURI;
+ let currentURI = Preferences.get("network.trr.uri").value;
+ let menu = document.getElementById("networkDnsOverHttpsResolverChoices");
+
+ // populate the DNS-Over-HTTPs resolver list
+ menu.removeAllItems();
+ for (let resolver of resolvers) {
+ let item = menu.appendItem(undefined, resolver.uri);
+ if (resolver.uri == defaultURI) {
+ document.l10n.setAttributes(
+ item,
+ "connection-dns-over-https-url-item-default",
+ {
+ name: resolver.UIName || resolver.uri,
+ }
+ );
+ } else {
+ item.label = resolver.UIName || resolver.uri;
+ }
+ }
+ let lastItem = menu.appendItem(undefined, "custom");
+ document.l10n.setAttributes(
+ lastItem,
+ "connection-dns-over-https-url-custom"
+ );
+
+ // set initial selection in the resolver provider picker
+ let selectedIndex = currentURI
+ ? resolvers.findIndex(r => r.uri == currentURI)
+ : 0;
+ if (selectedIndex == -1) {
+ // select the last "Custom" item
+ selectedIndex = menu.itemCount - 1;
+ }
+ menu.selectedIndex = selectedIndex;
+
+ if (this.isDnsOverHttpsLocked()) {
+ // disable all the options and the checkbox itself to disallow enabling them
+ this.toggleDnsOverHttpsUI(true);
+ document.getElementById("networkDnsOverHttps").disabled = true;
+ } else {
+ this.toggleDnsOverHttpsUI(false);
+ this.updateDnsOverHttpsUI();
+ document.getElementById("networkDnsOverHttps").disabled = false;
+ }
+ },
+
+ registerSyncPrefListeners() {
+ function setSyncFromPrefListener(element_id, callback) {
+ Preferences.addSyncFromPrefListener(
+ document.getElementById(element_id),
+ callback
+ );
+ }
+ function setSyncToPrefListener(element_id, callback) {
+ Preferences.addSyncToPrefListener(
+ 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)
+ );
+ setSyncFromPrefListener("networkDnsOverHttps", () =>
+ this.readDnsOverHttpsMode()
+ );
+ setSyncToPrefListener("networkDnsOverHttps", () =>
+ this.writeDnsOverHttpsMode()
+ );
+ },
+};
diff --git a/browser/components/preferences/dialogs/connection.xhtml b/browser/components/preferences/dialogs/connection.xhtml
new file mode 100644
index 0000000000..ffd3259537
--- /dev/null
+++ b/browser/components/preferences/dialogs/connection.xhtml
@@ -0,0 +1,159 @@
+<?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="-moz-box-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="-moz-box-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="-moz-box-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="-moz-box-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" />
+
+ <groupbox>
+ <checkbox id="networkDnsOverHttps"
+ data-l10n-id="connection-dns-over-https"
+ preference="network.trr.mode"/>
+
+ <box id="dnsOverHttps-grid" class="indent" flex="1">
+ <html:div class="dnsOverHttps-grid-row">
+ <hbox pack="end">
+ <label id="networkDnsOverHttpsResolverChoicesLabel"
+ data-l10n-id="connection-dns-over-https-url-resolver"
+ control="networkDnsOverHttpsResolverChoices"/>
+ </hbox>
+ <menulist id="networkDnsOverHttpsResolverChoices"
+ oncommand="gConnectionsDialog.updateDnsOverHttpsUI()"></menulist>
+ </html:div>
+ <html:div class="dnsOverHttps-grid-row" id="customDnsOverHttpsContainer" hidden="hidden">
+ <hbox pack="end">
+ <label id="networkCustomDnsOverHttpsInputLabel"
+ data-l10n-id="connection-dns-over-https-custom-label"
+ control="networkCustomDnsOverHttpsInput"/>
+ </hbox>
+ <html:input id="networkCustomDnsOverHttpsInput" type="text" style="-moz-box-flex: 1;"
+ preference="network.trr.custom_uri"/>
+ </html:div>
+ </box>
+ </groupbox>
+</dialog>
+</window>
diff --git a/browser/components/preferences/dialogs/containers.js b/browser/components/preferences/dialogs/containers.js
new file mode 100644
index 0000000000..7ceb7b2130
--- /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);
+ },
+
+ 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..a933f31d8a
--- /dev/null
+++ b/browser/components/preferences/dialogs/containers.xhtml
@@ -0,0 +1,53 @@
+<?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/fonts.js b/browser/components/preferences/dialogs/fonts.js
new file mode 100644
index 0000000000..1621147018
--- /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(Cu.reportError);
+ },
+
+ 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..777a65dd66
--- /dev/null
+++ b/browser/components/preferences/dialogs/fonts.xhtml
@@ -0,0 +1,222 @@
+<?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="right"?
+ This goes back to the beginning of time...
+ -->
+ <menulist id="monospace" crop="right" 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..8f7becd81d
--- /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: -moz-box;
+ 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..05050dc37b
--- /dev/null
+++ b/browser/components/preferences/dialogs/jar.mn
@@ -0,0 +1,45 @@
+# 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/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/translation.xhtml
+ content/browser/preferences/dialogs/translation.js
diff --git a/browser/components/preferences/dialogs/languages.js b/browser/components/preferences/dialogs/languages.js
new file mode 100644
index 0000000000..a81f6a3461
--- /dev/null
+++ b/browser/components/preferences/dialogs/languages.js
@@ -0,0 +1,382 @@
+/* -*- 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(Cu.reportError)
+ );
+
+ 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(Cu.reportError);
+ },
+
+ 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(Cu.reportError);
+ },
+
+ _getLocaleName(localeCode) {
+ if (!this._availableLanguagesList.length) {
+ this._loadAvailableLanguages();
+ }
+ 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
+ if (localeCode.split("-")[0] == this._availableLanguagesList[i].code) {
+ return this._availableLanguagesList[i].name;
+ }
+ }
+
+ return "";
+ },
+
+ 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..4f50089da5
--- /dev/null
+++ b/browser/components/preferences/dialogs/languages.xhtml
@@ -0,0 +1,67 @@
+<?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 flex="1" style="display: grid; grid-template-rows: 1fr auto; grid-template-columns: 1fr auto;">
+ <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..1d43f6f810
--- /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", "-moz-box-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", "-moz-box-flex: 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", "-moz-box-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..7518b93fdb
--- /dev/null
+++ b/browser/components/preferences/dialogs/permissions.xhtml
@@ -0,0 +1,83 @@
+<?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="-moz-box-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="-moz-box-flex: 3; width: 0"
+ onclick="gPermissionManager.buildPermissionsList(event.target)"/>
+ <treecol id="statusCol" data-l10n-id="permissions-status" style="-moz-box-flex: 1; width: 0"
+ data-isCurrentSortCol="true"
+ onclick="gPermissionManager.buildPermissionsList(event.target);"/>
+ </listheader>
+ <richlistbox id="permissionsBox" flex="1" 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..1b27d96147
--- /dev/null
+++ b/browser/components/preferences/dialogs/sanitize.xhtml
@@ -0,0 +1,73 @@
+<?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..c72a55016e
--- /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..6980a597dd
--- /dev/null
+++ b/browser/components/preferences/dialogs/selectBookmark.xhtml
@@ -0,0 +1,49 @@
+<?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..eba8ce5c02
--- /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..f725b19243
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataSettings.js
@@ -0,0 +1,337 @@
+/* -*- 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.defineModuleGetter(
+ this,
+ "DownloadUtils",
+ "resource://gre/modules/DownloadUtils.jsm"
+);
+
+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", `-moz-box-flex: ${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..65d0db5b31
--- /dev/null
+++ b/browser/components/preferences/dialogs/siteDataSettings.xhtml
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" 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="-moz-box-flex: 4; width: 50px" data-l10n-id="site-data-column-host" id="hostCol"/>
+ <treecol style="-moz-box-flex: 1; 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="-moz-box-flex: 2; width: 50px" data-l10n-id="site-data-column-storage" id="usageCol" data-isCurrentSortCol="true"/>
+ <treecol style="-moz-box-flex: 2; width: 50px" data-l10n-id="site-data-column-last-used" id="lastAccessedCol" />
+ </listheader>
+ <richlistbox seltype="multiple" id="sitesList" orient="vertical" flex="1"/>
+ </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..2bfd502c0d
--- /dev/null
+++ b/browser/components/preferences/dialogs/sitePermissions.css
@@ -0,0 +1,40 @@
+/* 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 {
+ overflow: hidden; /* Allows equal sizing combined with width="0" */
+ padding-inline-start: 7px;
+ -moz-box-align: center;
+}
+
+#permissionsBox {
+ min-height: 18em;
+ contain: size;
+}
+
+#siteCol,
+#statusCol,
+#permissionsBox > richlistitem {
+ min-height: 35px;
+}
+
+.website-status {
+ margin: 1px;
+ margin-inline-end: 5px;
+}
+
+#browserNotificationsPermissionExtensionContent,
+#permissionsDisableDescription {
+ margin-inline-start: 32px;
+}
+
+#permissionsDisableDescription {
+ color: var(--in-content-deemphasized-text);
+ 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..d6c6bff963
--- /dev/null
+++ b/browser/components/preferences/dialogs/sitePermissions.js
@@ -0,0 +1,604 @@
+/* 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.import(
+ "resource:///modules/SitePermissions.jsm"
+);
+
+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",
+ },
+ "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}`);
+ },
+ },
+};
+
+function Permission(principal, type, capability, l10nId) {
+ this.principal = principal;
+ this.origin = principal.origin;
+ this.type = type;
+ this.capability = capability;
+ this.l10nId = l10nId;
+}
+
+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,
+ _permissions: 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);
+
+ // Ignore unrelated permission types and permissions with unknown states.
+ if (
+ permission.type !== this._type ||
+ !PERMISSION_STATES.includes(permission.capability)
+ ) {
+ return;
+ }
+
+ if (data == "added") {
+ this._addPermissionToList(permission);
+ this.buildPermissionsList();
+ } else if (data == "changed") {
+ let p = this._permissions.get(permission.principal.origin);
+ p.capability = permission.capability;
+ p.l10nId = this._getCapabilityString(
+ permission.type,
+ permission.capability
+ );
+ this._handleCapabilityChange(p);
+ this.buildPermissionsList();
+ } else if (data == "deleted") {
+ this._removePermissionFromList(permission.principal.origin);
+ }
+ },
+
+ _handleCapabilityChange(perm) {
+ let permissionlistitem = document.getElementsByAttribute(
+ "origin",
+ perm.origin
+ )[0];
+ let menulist = permissionlistitem.getElementsByTagName("menulist")[0];
+ menulist.selectedItem = menulist.getElementsByAttribute(
+ "value",
+ perm.capability
+ )[0];
+ },
+
+ _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;
+ }
+ },
+
+ _getCapabilityString(type, capability) {
+ if (
+ type in sitePermissionsConfig &&
+ sitePermissionsConfig[type]._getCapabilityString
+ ) {
+ return sitePermissionsConfig[type]._getCapabilityString(capability);
+ }
+
+ 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}`);
+ }
+ },
+
+ _addPermissionToList(perm) {
+ // Ignore unrelated permission types and permissions with unknown states.
+ if (
+ perm.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 l10nId = this._getCapabilityString(perm.type, perm.capability);
+ let p = new Permission(perm.principal, perm.type, perm.capability, l10nId);
+ this._permissions.set(p.origin, p);
+ },
+
+ _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 width = "75px";
+ let richlistitem = document.createXULElement("richlistitem");
+ richlistitem.setAttribute("origin", permission.origin);
+ let row = document.createXULElement("hbox");
+ row.setAttribute("style", "-moz-box-flex: 1");
+
+ let hbox = document.createXULElement("hbox");
+ let website = document.createXULElement("label");
+ website.setAttribute("value", permission.origin);
+ // TODO(bug 1802993): Seems this could be on the hbox instead or something?
+ website.setAttribute("style", `width: ${width}`);
+ hbox.setAttribute("class", "website-name");
+ hbox.setAttribute("style", `-moz-box-flex: 3`);
+ hbox.appendChild(website);
+
+ let menulist = document.createXULElement("menulist");
+ menulist.setAttribute("style", `-moz-box-flex: 1; width: ${width}`);
+ menulist.setAttribute("class", "website-status");
+ let states = SitePermissions.getAvailableStates(permission.type);
+ for (let state of states) {
+ // Work around the (rare) edge case when a user has changed their
+ // default permission type back to UNKNOWN while still having a
+ // PROMPT permission set for an origin.
+ if (
+ state == SitePermissions.UNKNOWN &&
+ permission.capability == SitePermissions.PROMPT
+ ) {
+ state = SitePermissions.PROMPT;
+ } else if (state == SitePermissions.UNKNOWN) {
+ continue;
+ }
+ let m = menulist.appendItem(undefined, state);
+ document.l10n.setAttributes(
+ m,
+ this._getCapabilityString(permission.type, state)
+ );
+ }
+ menulist.value = permission.capability;
+
+ menulist.addEventListener("select", () => {
+ this.onPermissionChange(permission, Number(menulist.value));
+ });
+
+ row.appendChild(hbox);
+ row.appendChild(menulist);
+ 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 permission = this._permissions.get(origin);
+
+ this._removePermissionFromList(origin);
+ this._permissionsToDelete.set(permission.origin, permission);
+
+ this._setRemoveButtonState();
+ },
+
+ onAllPermissionsDelete() {
+ for (let permission of this._permissions.values()) {
+ this._removePermissionFromList(permission.origin);
+ this._permissionsToDelete.set(permission.origin, permission);
+ }
+
+ this._setRemoveButtonState();
+ },
+
+ onPermissionSelect() {
+ this._setRemoveButtonState();
+ },
+
+ onPermissionChange(perm, capability) {
+ let p = this._permissions.get(perm.origin);
+ if (p.capability == capability) {
+ return;
+ }
+ p.capability = capability;
+ p.l10nId = this._getCapabilityString(perm.type, perm.capability);
+ this._permissionsToChange.set(p.origin, p);
+
+ // 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();
+
+ for (let p of this._permissionsToChange.values()) {
+ SitePermissions.setForPrincipal(p.principal, p.type, p.capability);
+ }
+
+ for (let p of this._permissionsToDelete.values()) {
+ SitePermissions.removeFromPrincipal(p.principal, p.type);
+ }
+
+ 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 permissions = Array.from(this._permissions.values());
+
+ let keyword = this._searchBox.value.toLowerCase().trim();
+ for (let permission of permissions) {
+ if (keyword && !permission.origin.includes(keyword)) {
+ continue;
+ }
+
+ let richlistitem = this._createPermissionListItem(permission);
+ 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._getCapabilityString("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("menulist").value) >
+ parseInt(b.querySelector("menulist").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..0f50fae9a2
--- /dev/null
+++ b/browser/components/preferences/dialogs/sitePermissions.xhtml
@@ -0,0 +1,82 @@
+<?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" style="-moz-box-flex: 3; width: 75px"
+ onclick="gSitePermissionsManager.buildPermissionsList(event.target)"/>
+ <treecol id="statusCol" data-l10n-id="permissions-status" style="-moz-box-flex: 1; width: 75px"
+ data-isCurrentSortCol="true"
+ onclick="gSitePermissionsManager.buildPermissionsList(event.target);"/>
+ </listheader>
+ <richlistbox id="permissionsBox" flex="1" 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..d1f468233a
--- /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..35b1b6c2cd
--- /dev/null
+++ b/browser/components/preferences/dialogs/syncChooseWhatToSync.xhtml
@@ -0,0 +1,63 @@
+<?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/branding/sync-brand.ftl"/>
+ <html:link rel="localization" href="browser/preferences/preferences.ftl"/>
+ </linkset>
+ <script src="chrome://global/content/preferencesBindings.js"/>
+ <script src="chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.js"/>
+
+ <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/translation.js b/browser/components/preferences/dialogs/translation.js
new file mode 100644
index 0000000000..1b94f3a3dd
--- /dev/null
+++ b/browser/components/preferences/dialogs/translation.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/. */
+
+"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/translation.xhtml b/browser/components/preferences/dialogs/translation.xhtml
new file mode 100644
index 0000000000..79bff31974
--- /dev/null
+++ b/browser/components/preferences/dialogs/translation.xhtml
@@ -0,0 +1,87 @@
+<?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="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/translation.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>