diff options
Diffstat (limited to 'browser/components/preferences/dialogs')
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> |